From 28bb3367cb11bf91aadf9d0b7b88957f7df85339 Mon Sep 17 00:00:00 2001 From: Oleg Kosenkov Date: Sun, 6 Nov 2022 17:58:44 -0500 Subject: [PATCH] Games: Add ColorLines --- Base/res/apps/ColorLines.af | 4 + Base/res/icons/16x16/app-colorlines.png | Bin 0 -> 590 bytes Base/res/icons/32x32/app-colorlines.png | Bin 0 -> 1316 bytes Base/res/icons/colorlines/colorlines.png | Bin 0 -> 24414 bytes Base/usr/share/man/man6/ColorLines.md | 19 ++ Userland/Games/CMakeLists.txt | 1 + Userland/Games/ColorLines/CMakeLists.txt | 13 + Userland/Games/ColorLines/ColorLines.cpp | 400 +++++++++++++++++++++++ Userland/Games/ColorLines/ColorLines.h | 90 +++++ Userland/Games/ColorLines/HueFilter.h | 51 +++ Userland/Games/ColorLines/Marble.h | 46 +++ Userland/Games/ColorLines/MarbleBoard.h | 356 ++++++++++++++++++++ Userland/Games/ColorLines/MarblePath.h | 64 ++++ Userland/Games/ColorLines/main.cpp | 75 +++++ 14 files changed, 1119 insertions(+) create mode 100644 Base/res/apps/ColorLines.af create mode 100644 Base/res/icons/16x16/app-colorlines.png create mode 100644 Base/res/icons/32x32/app-colorlines.png create mode 100644 Base/res/icons/colorlines/colorlines.png create mode 100644 Base/usr/share/man/man6/ColorLines.md create mode 100644 Userland/Games/ColorLines/CMakeLists.txt create mode 100644 Userland/Games/ColorLines/ColorLines.cpp create mode 100644 Userland/Games/ColorLines/ColorLines.h create mode 100644 Userland/Games/ColorLines/HueFilter.h create mode 100644 Userland/Games/ColorLines/Marble.h create mode 100644 Userland/Games/ColorLines/MarbleBoard.h create mode 100644 Userland/Games/ColorLines/MarblePath.h create mode 100644 Userland/Games/ColorLines/main.cpp diff --git a/Base/res/apps/ColorLines.af b/Base/res/apps/ColorLines.af new file mode 100644 index 0000000000..32244649a5 --- /dev/null +++ b/Base/res/apps/ColorLines.af @@ -0,0 +1,4 @@ +[App] +Name=Color Lines +Executable=/bin/ColorLines +Category=Games diff --git a/Base/res/icons/16x16/app-colorlines.png b/Base/res/icons/16x16/app-colorlines.png new file mode 100644 index 0000000000000000000000000000000000000000..737b10838cf96cd2b07d4fe4facd758d559e8da0 GIT binary patch literal 590 zcmeAS@N?(olHy`uVBq!ia0y~yU=RRd4rT@h1`S>QUZ`S^)KU({|vvsOME!D=Jcs!rzX`-P+*pGVQ|P}NT0yazLsI}Nrt_587{nH zxciym=_iJ#S4z(wJ9M-!wo8>kS&c#2m%%fiA+v{}Wd*~$T?`veG3>w2aQYgvs&Y&vIAgRfvHo0Tc$-^fuw46GvbZ{Qy)H;T;AST~vh48r@v(Iceb*Ae0 z{^*^XW7ck}UVCukfm26M9@&3**S4LNaWhpJ7#Jpbx;TbNNQNHVn0v^9!QtY6|Mrsd zZQfpN4vW%x(mbVSDAfMC64~6xnjxs#E4`t`MI{h~`(gWLVNo?d^iatCj(vQtac`mMGqt{PDfxm{M&RGtxG`ZF!)o8@!wbM^ua zQ!c5^zob>qypEsI?oF-Zu8sfJ+{;OQenmJzPdxA5EaNi_3=9mOu6{1-oD!McAVJL+}uR)A%X;f<7sp__~(_H6tfC$&jP zLU6Kc7-whVqlp`OMXiIBjNI0H%xYiux-kFls%Z1>ZzuG(x|;=`est~s+~DVTKFe?1 zvZdtJ70qnj-M=_ma^HUXw5k5rmzSTPpMP(4AgcQ7sjJ1$&;8839hJR4c5hhb-fqq* zt{aOAU9;c%$Av_#4tkr!mKr(h6vqeor&~7rr5!rGG~D~;lJ1z>gDcaEW_`dGQYp?^?%=YH(#~B z;p9|qn&2@dbBCVw^yh8jv;MqXE4_c`O!=^m_SYve8=~d!?)h^2Y4!ViiKkgv6r$u@ ze*K(3ajE_1>j#DPZ4CGCNuSkMlkw*N&jt1@Ex{NuSnCg9JGPhMDc| zb>%hh_R77x$WWetf9Hp1)<50y47hR)SW`VC1Ja&I?p)Z?r7z@^@&3eWcdPlX_fu+Q z7i|6a_-A?e|KB%N3R+K{6wO*RZ}Rgetgf>yejO0Me);sH+I-$6V%8!*k8OSZ|Hto5 z8JZeKpE@*DcsQAx*o_%F{+Qg9-Zp!w!>QbfC90lwT;1~F!Hl`L<9>cPwfA;;@UF=T z$%{Hp9RL1f&vSK#>39A9mD($G>^oAHDjoObl(9uU*MTQ#Zzu1b_t|(;Ph9&(e*qz` zmcynd4;(CRaZO&3{HWu?;m;pgO(K40`m+7iY;e2!t;*Q>z1sF|66`;2Z?rDpV>gND zeK#p9#7SMLi=&3AKy=xzm4TD?RaiJ&X?a}GSLbJVkL|~gzHJAkUQN4}BpuZ3>0xrU zthHvrLe1X3SMz>MdK1nNRKfh~GmpqCvH!OdEyn1uhvVoBZ3!Ini~!zX|KB3TzgS4 zm~oNvbN?Q`z2yaqUN|Vd-?;5y+ai-0ZOfJgt>{_gEhXUdD?&;A$Ir>!ao6Y9U9`Dm zy!Y^X8I##lmwlSRRP)F5nrYSC($-TiRgV}Jdd-^Tv8N+INqbd+2(KUiQuY0%G4m%Z zp0Brpd(|>dPKP5$3_s5JI3vd=Y>}0UVWH170smV(E0$RPzH{^8!sxM4`Iyg4mU_LH3|L4=qjv{YzEXtlN3V*bC zz0TR!2X8XlSJuD1bo^COyNdGrOLKc{TGw6RT65vkh*(;Ly=F`{>$g-b%kG^B;P-4v|9ewH(v37j!Wua z&be@$?Th)%UFZJ_O`R-0MNGPP(eZiK|DLR$cWZO`x81Y%Ex32zOuy?#`pXwVclO&% zk3C-$Dk8EhdV9BCrP17<7vG%u7^(kh-~X_OO3VGO{b88dzH*lLtDB6vj7k$1X)m|_ z<;?Ktwx8!M$>Tf<@}6I_8M1RDMN%W*JxW`?wd`Eu2lePzr;dsS&x~cUc)n(<6W7Ka sJ9gwJ&c7D*=3P~;B2toUe^eSa$OZ0|Nttr>mdKI;Vst07(#^%K!iX literal 0 HcmV?d00001 diff --git a/Base/res/icons/colorlines/colorlines.png b/Base/res/icons/colorlines/colorlines.png new file mode 100644 index 0000000000000000000000000000000000000000..7be363222a3b8592759b528f80b5fc39c21a83ff GIT binary patch literal 24414 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 GLK6VWrF#(o literal 0 HcmV?d00001 diff --git a/Base/usr/share/man/man6/ColorLines.md b/Base/usr/share/man/man6/ColorLines.md new file mode 100644 index 0000000000..95d1b38cdd --- /dev/null +++ b/Base/usr/share/man/man6/ColorLines.md @@ -0,0 +1,19 @@ +## Name + +![Icon](/res/icons/16x16/app-colorlines.png) Color Lines + +[Open](file:///bin/ColorLines) + +## Synopsis + +```**sh +$ ColorLines +``` + +## Description + +ColorLines is a classic game. + +Click a marble, then click an empty square to move. +You can only move along unblocked paths. +Build rows of 5 or more marbles of the same color to score. diff --git a/Userland/Games/CMakeLists.txt b/Userland/Games/CMakeLists.txt index 8a7aae6b5b..85250adfcd 100644 --- a/Userland/Games/CMakeLists.txt +++ b/Userland/Games/CMakeLists.txt @@ -1,6 +1,7 @@ add_subdirectory(2048) add_subdirectory(BrickGame) add_subdirectory(Chess) +add_subdirectory(ColorLines) add_subdirectory(FlappyBug) add_subdirectory(Flood) add_subdirectory(GameOfLife) diff --git a/Userland/Games/ColorLines/CMakeLists.txt b/Userland/Games/ColorLines/CMakeLists.txt new file mode 100644 index 0000000000..12ee78ddb5 --- /dev/null +++ b/Userland/Games/ColorLines/CMakeLists.txt @@ -0,0 +1,13 @@ +serenity_component( + ColorLines + RECOMMENDED + TARGETS ColorLines +) + +set(SOURCES + ColorLines.cpp + main.cpp +) + +serenity_app(ColorLines ICON app-colorlines) +target_link_libraries(ColorLines PRIVATE LibGUI LibCore LibGfx LibConfig LibMain LibDesktop) diff --git a/Userland/Games/ColorLines/ColorLines.cpp b/Userland/Games/ColorLines/ColorLines.cpp new file mode 100644 index 0000000000..f8b4b095f3 --- /dev/null +++ b/Userland/Games/ColorLines/ColorLines.cpp @@ -0,0 +1,400 @@ +/* + * Copyright (c) 2022, Oleg Kosenkov + * Copyright (c) 2022, the SerenityOS developers. + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include "ColorLines.h" +#include "HueFilter.h" +#include "Marble.h" +#include "MarbleBoard.h" +#include +#include +#include +#include +#include + +ColorLines::BitmapArray ColorLines::build_marble_color_bitmaps() +{ + auto marble_bitmap = MUST(Gfx::Bitmap::try_load_from_file("/res/icons/colorlines/colorlines.png"sv)); + float constexpr hue_degrees[Marble::number_of_colors] = { + 0, // Red + 45, // Brown/Yellow + 90, // Green + 180, // Cyan + 225, // Blue + 300 // Purple + }; + BitmapArray colored_bitmaps; + colored_bitmaps.ensure_capacity(Marble::number_of_colors); + for (int i = 0; i < Marble::number_of_colors; ++i) { + auto bitmap = MUST(marble_bitmap->clone()); + HueFilter filter { hue_degrees[i] }; + filter.apply(*bitmap, bitmap->rect(), *marble_bitmap, marble_bitmap->rect()); + colored_bitmaps.append(bitmap); + } + return colored_bitmaps; +} + +ColorLines::BitmapArray ColorLines::build_marble_trace_bitmaps() +{ + // Use "Paw Prints" Unicode Character (U+1F43E) + auto trace_bitmap = NonnullRefPtr(*Gfx::Emoji::emoji_for_code_point(0x1F43E)); + BitmapArray result; + result.ensure_capacity(number_of_marble_trace_bitmaps); + result.append(trace_bitmap); + result.append(MUST(result.last()->rotated(Gfx::RotationDirection::Clockwise))); + result.append(MUST(result.last()->rotated(Gfx::RotationDirection::Clockwise))); + result.append(MUST(result.last()->rotated(Gfx::RotationDirection::Clockwise))); + return result; +} + +ColorLines::ColorLines(StringView app_name) + : m_app_name { app_name } + , m_game_state { GameState::Idle } + , m_board { make() } + , m_marble_bitmaps { build_marble_color_bitmaps() } + , m_trace_bitmaps { build_marble_trace_bitmaps() } + , m_score_font { Gfx::BitmapFont::load_from_file("/res/fonts/MarietaBold24.font") } +{ + VERIFY(m_marble_bitmaps.size() == Marble::number_of_colors); + set_font(Gfx::FontDatabase::default_fixed_width_font().bold_variant()); + m_high_score = Config::read_i32(m_app_name, m_app_name, "HighScore"sv, 0); + reset(); +} + +void ColorLines::reset() +{ + set_game_state(GameState::StartingGame); +} + +void ColorLines::mousedown_event(GUI::MouseEvent& event) +{ + if (m_game_state != GameState::Idle && m_game_state != GameState::MarbleSelected) + return; + auto const event_position = event.position().translated( + -frame_inner_rect().x(), + -frame_inner_rect().y() - board_vertical_margin); + if (event_position.x() < 0 || event_position.y() < 0) + return; + auto const clicked_cell = Point { event_position.x() / board_cell_dimension.width(), + event_position.y() / board_cell_dimension.height() }; + if (!MarbleBoard::in_bounds(clicked_cell)) + return; + if (m_board->has_selected_marble()) { + auto const selected_cell = m_board->selected_marble().position(); + if (selected_cell == clicked_cell) { + m_board->reset_selection(); + set_game_state(GameState::Idle); + return; + } + if (m_board->is_empty_cell_at(clicked_cell)) { + if (m_board->build_marble_path(selected_cell, clicked_cell, m_marble_path)) + set_game_state(GameState::MarbleMoving); + return; + } + if (m_board->select_marble(clicked_cell)) + set_game_state(GameState::MarbleSelected); + return; + } + if (m_board->select_marble(clicked_cell)) + set_game_state(GameState::MarbleSelected); +} + +void ColorLines::timer_event(Core::TimerEvent&) +{ + switch (m_game_state) { + case GameState::GeneratingMarbles: + update(); + if (--m_marble_animation_frame < AnimationFrames::marble_generating_end) { + m_marble_animation_frame = AnimationFrames::marble_default; + set_game_state(GameState::CheckingMarbles); + } + break; + + case GameState::MarbleSelected: + m_marble_animation_frame = (m_marble_animation_frame + 1) % AnimationFrames::number_of_marble_bounce_frames; + update(); + break; + + case GameState::MarbleMoving: + m_marble_animation_frame = (m_marble_animation_frame + 1) % AnimationFrames::number_of_marble_bounce_frames; + update(); + if (m_marble_path.remaining_steps() != 1 && m_marble_animation_frame != AnimationFrames::marble_at_top) + break; + if (auto const point = m_marble_path.next_point(); m_marble_path.is_empty()) { + auto const color = m_board->selected_marble().color(); + m_board->reset_selection(); + m_board->set_color_at(point, color); + if (m_board->check_and_remove_marbles()) + set_game_state(GameState::MarblesRemoving); + else + set_game_state(GameState::GeneratingMarbles); + } + break; + + case GameState::MarblesRemoving: + update(); + if (++m_marble_animation_frame > AnimationFrames::marble_removing_end) { + m_marble_animation_frame = AnimationFrames::marble_default; + m_score += 2 * m_board->removed_marbles().size(); + set_game_state(GameState::Idle); + } + break; + + case GameState::StartingGame: + case GameState::Idle: + case GameState::CheckingMarbles: + break; + + case GameState::GameOver: { + stop_timer(); + update(); + StringBuilder text; + text.appendff("Your score is {}", m_score); + if (m_score > m_high_score) { + text.append("\nThis is a new high score!"sv); + Config::write_i32(m_app_name, m_app_name, "HighScore"sv, int(m_high_score = m_score)); + } + GUI::MessageBox::show(window(), + text.string_view(), + "Game Over"sv, + GUI::MessageBox::Type::Information); + reset(); + break; + } + + default: + VERIFY_NOT_REACHED(); + } +} + +void ColorLines::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()); + + auto paint_cell = [&](GUI::Painter& painter, Gfx::IntRect rect, int color, int animation_frame) { + painter.draw_rect(rect, Color::Black); + rect.shrink(0, 1, 1, 0); + painter.draw_line(rect.bottom_left(), rect.top_left(), Color::White); + painter.draw_line(rect.top_left(), rect.top_right(), Color::White); + painter.draw_line(rect.top_right(), rect.bottom_right(), Color::DarkGray); + painter.draw_line(rect.bottom_right(), rect.bottom_left(), Color::DarkGray); + rect.shrink(1, 1, 1, 1); + painter.draw_line(rect.bottom_left(), rect.top_left(), Color::LightGray); + painter.draw_line(rect.top_left(), rect.top_right(), Color::LightGray); + painter.draw_line(rect.top_right(), rect.bottom_right(), Color::MidGray); + painter.draw_line(rect.bottom_right(), rect.bottom_left(), Color::MidGray); + rect.shrink(1, 1, 1, 1); + painter.fill_rect(rect, tile_color); + rect.shrink(1, 1, 1, 1); + if (color >= 0 && color < Marble::number_of_colors) { + auto const source_rect = Gfx::IntRect { animation_frame * marble_pixel_size, 0, marble_pixel_size, marble_pixel_size }; + painter.draw_scaled_bitmap(rect, *m_marble_bitmaps[color], source_rect, + 1.0f, Gfx::Painter::ScalingMode::BilinearBlend); + } + }; + + painter.set_font(*m_score_font); + + // Draw board header with score, high score + auto board_header_size = frame_inner_rect().size(); + board_header_size.set_height(board_vertical_margin); + auto const board_header_rect = Gfx::IntRect { frame_inner_rect().top_left(), board_header_size }; + painter.fill_rect(board_header_rect, Color::Black); + + auto const text_margin = 8; + + // Draw score + auto const score_text = MUST(String::formatted("{:05}"sv, m_score)); + auto text_width { m_score_font->width(score_text) }; + auto const glyph_height = m_score_font->glyph_height(); + auto const score_text_rect = Gfx::IntRect { + frame_inner_rect().top_left().translated(text_margin), + Gfx::IntSize { text_width, glyph_height } + }; + painter.draw_text(score_text_rect, score_text, Gfx::TextAlignment::CenterLeft, text_color); + + // Draw high score + auto const high_score_text = MUST(String::formatted("{:05}"sv, m_high_score)); + text_width = m_score_font->width(high_score_text); + auto const high_score_text_rect = Gfx::IntRect { + frame_inner_rect().top_right().translated(-(text_margin + text_width), text_margin), + Gfx::IntSize { text_width, glyph_height } + }; + painter.draw_text(high_score_text_rect, high_score_text, Gfx::TextAlignment::CenterLeft, text_color); + + auto const cell_rect + = Gfx::IntRect(frame_inner_rect().top_left(), board_cell_dimension) + .translated(0, board_vertical_margin); + + // Draw all cells and the selected marble if it exists + for (int y = 0; y < MarbleBoard::board_size.height(); ++y) + for (int x = 0; x < MarbleBoard::board_size.width(); ++x) { + auto const& destination_rect = cell_rect.translated( + x * board_cell_dimension.width(), + y * board_cell_dimension.height()); + auto const point = Point { x, y }; + auto const animation_frame = m_game_state == GameState::MarbleSelected && m_board->has_selected_marble() + && m_board->selected_marble().position() == point + ? m_marble_animation_frame + : AnimationFrames::marble_default; + paint_cell(painter, destination_rect, m_board->color_at(point), animation_frame); + } + + // Draw preview marbles in the board + for (auto const& marble : m_board->preview_marbles()) { + auto const& point = marble.position(); + if (m_marble_path.contains(point) || !m_board->is_empty_cell_at(point)) + continue; + auto const& destination_rect = cell_rect.translated( + point.x() * board_cell_dimension.width(), + point.y() * board_cell_dimension.height()); + auto get_animation_frame = [this]() -> int { + switch (m_game_state) { + case GameState::GameOver: + return AnimationFrames::marble_default; + case GameState::GeneratingMarbles: + case GameState::CheckingMarbles: + return m_marble_animation_frame; + default: + return AnimationFrames::marble_generating_start; + } + }; + paint_cell(painter, destination_rect, marble.color(), get_animation_frame()); + } + + // Draw preview marbles in the board header + for (size_t i = 0; i < MarbleBoard::number_of_preview_marbles; ++i) { + auto const& marble = m_board->preview_marbles()[i]; + auto const& destination_rect = cell_rect.translated( + int(i + 3) * board_cell_dimension.width(), + -board_vertical_margin) + .shrunken(10, 10); + paint_cell(painter, destination_rect, marble.color(), AnimationFrames::marble_preview); + } + + // Draw moving marble + if (!m_marble_path.is_empty()) { + auto const point = m_marble_path.current_point(); + auto const& destination_rect = cell_rect.translated( + point.x() * board_cell_dimension.width(), + point.y() * board_cell_dimension.height()); + paint_cell(painter, destination_rect, m_board->selected_marble().color(), m_marble_animation_frame); + } + + // Draw removing marble + if (m_game_state == GameState::MarblesRemoving) + for (auto const& marble : m_board->removed_marbles()) { + auto const& point = marble.position(); + auto const& destination_rect = cell_rect.translated( + point.x() * board_cell_dimension.width(), + point.y() * board_cell_dimension.height()); + paint_cell(painter, destination_rect, marble.color(), m_marble_animation_frame); + } + + // Draw marble move trace + if (m_game_state == GameState::MarbleMoving && m_marble_path.remaining_steps() > 1) { + auto const trace_size = Gfx::IntSize { m_trace_bitmaps.first()->width(), m_trace_bitmaps.first()->height() }; + auto const target_trace_size = Gfx::IntSize { 14, 14 }; + auto const source_rect = Gfx::FloatRect(Gfx::IntPoint {}, trace_size); + for (size_t i = 0; i < m_marble_path.remaining_steps() - 1; ++i) { + auto const& current_step = m_marble_path[i]; + auto const destination_rect = Gfx::IntRect(frame_inner_rect().top_left(), target_trace_size) + .translated( + current_step.x() * board_cell_dimension.width(), + board_vertical_margin + current_step.y() * board_cell_dimension.height()) + .translated( + (board_cell_dimension.width() - target_trace_size.width()) / 2, + (board_cell_dimension.height() - target_trace_size.height()) / 2); + auto get_direction_bitmap_index = [&]() -> size_t { + auto const& previous_step = m_marble_path[i + 1]; + if (previous_step.x() > current_step.x()) + return 3; + if (previous_step.x() < current_step.x()) + return 1; + if (previous_step.y() > current_step.y()) + return 0; + return 2; + }; + painter.draw_scaled_bitmap(destination_rect, *m_trace_bitmaps[get_direction_bitmap_index()], source_rect, + 1.0f, Gfx::Painter::ScalingMode::BilinearBlend); + } + } +} + +void ColorLines::restart_timer(int milliseconds) +{ + stop_timer(); + start_timer(milliseconds); +} + +void ColorLines::set_game_state(GameState state) +{ + m_game_state = state; + switch (state) { + case GameState::StartingGame: + m_marble_path.reset(); + m_board->reset(); + m_score = 0; + m_marble_animation_frame = AnimationFrames::marble_default; + update(); + if (m_board->update_preview_marbles(false)) + set_game_state(GameState::GeneratingMarbles); + else + set_game_state(GameState::GameOver); + break; + case GameState::GeneratingMarbles: + m_board->reset_selection(); + m_marble_animation_frame = AnimationFrames::marble_generating_start; + update(); + if (m_board->ensure_all_preview_marbles_are_on_empty_cells()) + restart_timer(TimerIntervals::generating_marbles); + else + set_game_state(GameState::GameOver); + break; + case GameState::MarblesRemoving: + m_marble_animation_frame = AnimationFrames::marble_removing_start; + update(); + restart_timer(TimerIntervals::removing_marbles); + break; + case GameState::Idle: + m_marble_animation_frame = AnimationFrames::marble_default; + update(); + if (m_board->ensure_all_preview_marbles_are_on_empty_cells() && m_board->has_empty_cells()) + stop_timer(); + else + set_game_state(GameState::GameOver); + break; + case GameState::MarbleSelected: + restart_timer(TimerIntervals::selected_marble); + m_marble_animation_frame = AnimationFrames::marble_default; + update(); + break; + case GameState::CheckingMarbles: + m_marble_animation_frame = AnimationFrames::marble_default; + update(); + if (!m_board->place_preview_marbles_on_board()) + set_game_state(GameState::GameOver); + else if (m_board->check_and_remove_marbles()) + set_game_state(GameState::MarblesRemoving); + else + set_game_state(GameState::Idle); + break; + case GameState::MarbleMoving: + restart_timer(TimerIntervals::moving_marble); + m_board->clear_color_at(m_board->selected_marble().position()); + update(); + break; + case GameState::GameOver: + m_marble_animation_frame = AnimationFrames::marble_default; + update(); + break; + default: + VERIFY_NOT_REACHED(); + } +} diff --git a/Userland/Games/ColorLines/ColorLines.h b/Userland/Games/ColorLines/ColorLines.h new file mode 100644 index 0000000000..0197080720 --- /dev/null +++ b/Userland/Games/ColorLines/ColorLines.h @@ -0,0 +1,90 @@ +/* + * Copyright (c) 2022, Oleg Kosenkov + * Copyright (c) 2022, the SerenityOS developers. + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include "MarblePath.h" +#include +#include +#include +#include +#include +#include + +class MarbleBoard; + +class ColorLines : public GUI::Frame { + C_OBJECT(ColorLines); + +public: + virtual ~ColorLines() override = default; + + void reset(); + +private: + enum class GameState { + Idle = 0, // No marble is selected, waiting for marble selection + StartingGame, // Game is starting + GeneratingMarbles, // Three new marbles are being generated + MarbleSelected, // Marble is selected, waiting for the target cell selection + MarbleMoving, // Selected marble is moving to the target cell + MarblesRemoving, // Selected marble has completed the move and some marbles are being removed from the board + CheckingMarbles, // Checking whether marbles on the board form lines of 5 or more marbles + GameOver // Game is over + }; + + ColorLines(StringView app_name); + + virtual void paint_event(GUI::PaintEvent&) override; + virtual void mousedown_event(GUI::MouseEvent&) override; + virtual void timer_event(Core::TimerEvent&) override; + + void set_game_state(GameState state); + void restart_timer(int milliseconds); + + using Point = Gfx::IntPoint; + using BitmapArray = Vector>; + + StringView const m_app_name; + GameState m_game_state { GameState::Idle }; + NonnullOwnPtr m_board; + BitmapArray const m_marble_bitmaps; + BitmapArray const m_trace_bitmaps; + RefPtr m_score_font; + MarblePath m_marble_path {}; + int m_marble_animation_frame {}; + unsigned m_score {}; + unsigned m_high_score {}; + + static BitmapArray build_marble_color_bitmaps(); + static BitmapArray build_marble_trace_bitmaps(); + + static constexpr auto marble_pixel_size { 40 }; + static constexpr auto board_vertical_margin { 45 }; + static constexpr auto board_cell_dimension = Gfx::IntSize { 48, 48 }; + static constexpr auto number_of_marble_trace_bitmaps { 4 }; + static constexpr auto tile_color { Color::from_rgb(0xc0c0c0) }; + static constexpr auto text_color { Color::from_rgb(0x00a0ff) }; + + enum AnimationFrames { + marble_default = 0, + marble_at_top = 2, + marble_preview = 18, + marble_generating_start = 21, + marble_generating_end = 17, + marble_removing_start = 7, + marble_removing_end = 16, + number_of_marble_bounce_frames = 7 + }; + + enum TimerIntervals { + generating_marbles = 80, + removing_marbles = 60, + selected_marble = 70, + moving_marble = 28 + }; +}; diff --git a/Userland/Games/ColorLines/HueFilter.h b/Userland/Games/ColorLines/HueFilter.h new file mode 100644 index 0000000000..71c70aa970 --- /dev/null +++ b/Userland/Games/ColorLines/HueFilter.h @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2022, Oleg Kosenkov + * Copyright (c) 2022, the SerenityOS developers. + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include +#include + +// This filter is similar to LibGfx/Filters/HueRotateFilter.h, however it uses +// a different formula (matrix) for hue rotation. This filter provides brighter +// colors compared to the filter provided in LibGfx. +class HueFilter : public Gfx::MatrixFilter { +public: + HueFilter(float angle_degrees) + : Gfx::MatrixFilter(calculate_hue_rotate_matrix(angle_degrees)) + { + } + + virtual bool amount_handled_in_filter() const override + { + return true; + } + + virtual StringView class_name() const override { return "HueFilter"sv; } + +private: + static FloatMatrix3x3 calculate_hue_rotate_matrix(float angle_degrees) + { + float const angle_rads = angle_degrees * (AK::Pi / 180.0f); + float cos_angle = 0.; + float sin_angle = 0.; + AK::sincos(angle_rads, sin_angle, cos_angle); + return FloatMatrix3x3 { + float(cos_angle + (1.0f - cos_angle) / 3.0f), + float(1.0f / 3.0f * (1.0f - cos_angle) - sqrtf(1.0f / 3.0f) * sin_angle), + float(1.0f / 3.0f * (1.0f - cos_angle) + sqrtf(1.0f / 3.0f) * sin_angle), + + float(1.0f / 3.0f * (1.0f - cos_angle) + sqrtf(1.0f / 3.0f) * sin_angle), + float(cos_angle + 1.0f / 3.0f * (1.0f - cos_angle)), + float(1.0f / 3.0f * (1.0f - cos_angle) - sqrtf(1.0f / 3.0f) * sin_angle), + + float(1.0f / 3.0f * (1.0f - cos_angle) - sqrtf(1.0f / 3.0f) * sin_angle), + float(1.0f / 3.0f * (1.0f - cos_angle) + sqrtf(1.0f / 3.0f) * sin_angle), + float(cos_angle + 1.0f / 3.0f * (1.0f - cos_angle)) + }; + } +}; diff --git a/Userland/Games/ColorLines/Marble.h b/Userland/Games/ColorLines/Marble.h new file mode 100644 index 0000000000..5ec2238ad0 --- /dev/null +++ b/Userland/Games/ColorLines/Marble.h @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2022, Oleg Kosenkov + * Copyright (c) 2022, the SerenityOS developers. + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include + +class Marble final { +public: + using Point = Gfx::IntPoint; + using Color = u8; + + static constexpr int number_of_colors { 6 }; + static constexpr Color empty_cell = NumericLimits::max(); + + Marble() = default; + Marble(Point position, Color color) + : m_position { position } + , m_color { color } + { + } + + bool operator==(Marble const& other) const = default; + + [[nodiscard]] constexpr Point position() const { return m_position; } + + [[nodiscard]] constexpr Color color() const { return m_color; } + +private: + Point m_position {}; + Color m_color {}; +}; + +namespace AK { +template<> +struct Traits : public GenericTraits { + static unsigned hash(Marble const& marble) + { + return Traits::hash(marble.position()); + } +}; +} diff --git a/Userland/Games/ColorLines/MarbleBoard.h b/Userland/Games/ColorLines/MarbleBoard.h new file mode 100644 index 0000000000..f5550d69d1 --- /dev/null +++ b/Userland/Games/ColorLines/MarbleBoard.h @@ -0,0 +1,356 @@ +/* + * Copyright (c) 2022, Oleg Kosenkov + * Copyright (c) 2022, the SerenityOS developers. + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include "Marble.h" +#include "MarblePath.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +class MarbleBoard final { +public: + using Color = Marble::Color; + using Point = Gfx::IntPoint; + using PointArray = Vector; + using SelectedMarble = Marble; + using PreviewMarble = Marble; + using MarbleArray = Vector; + + static constexpr Gfx::IntSize board_size { 9, 9 }; + static constexpr size_t number_of_preview_marbles = 3; + static constexpr Color empty_cell = Marble::empty_cell; + + using PreviewMarbles = Array; + + MarbleBoard() + { + reset(); + } + + ~MarbleBoard() = default; + + MarbleBoard(MarbleBoard const&) = delete; + + [[nodiscard]] bool has_empty_cells() const + { + bool result = false; + for_each_cell([&](Point point) { + result = is_empty_cell_at(point); + return result ? IterationDecision::Break : IterationDecision::Continue; + }); + return result; + } + + [[nodiscard]] PointArray get_empty_cells() const + { + PointArray result; + for_each_cell([&](Point point) { + if (is_empty_cell_at(point)) + result.append(point); + return IterationDecision::Continue; + }); + random_shuffle(result); + return result; + } + + void set_preview_marble(size_t i, PreviewMarble const& marble) + { + VERIFY(i < number_of_preview_marbles); + m_preview_marbles[i] = marble; + } + + [[nodiscard]] bool place_preview_marbles_on_board() + { + if (!ensure_all_preview_marbles_are_on_empty_cells()) + return false; + for (auto const& marble : m_preview_marbles) + if (!place_preview_marble_on_board(marble)) + return false; + return true; + } + + [[nodiscard]] bool check_preview_marbles_are_valid() + { + // Check marbles pairwise and also check the board cell under this marble is empty + static_assert(number_of_preview_marbles == 3); + return m_preview_marbles[0].position() != m_preview_marbles[1].position() && m_preview_marbles[0].position() != m_preview_marbles[2].position() + && m_preview_marbles[1].position() != m_preview_marbles[2].position() + && is_empty_cell_at(m_preview_marbles[0].position()) + && is_empty_cell_at(m_preview_marbles[1].position()) + && is_empty_cell_at(m_preview_marbles[2].position()); + } + + [[nodiscard]] bool update_preview_marbles(bool use_current) + { + auto empty_cells = get_empty_cells(); + for (size_t i = 0; i < number_of_preview_marbles; ++i) { + auto marble = m_preview_marbles[i]; + // Check marbles pairwise and also check the board cell under this marble is empty + auto const is_valid_marble = [&]() { + switch (i) { + case 0: + return marble.position() != m_preview_marbles[1].position() && marble.position() != m_preview_marbles[2].position() && is_empty_cell_at(marble.position()); + case 1: + return marble.position() != m_preview_marbles[0].position() && marble.position() != m_preview_marbles[2].position() && is_empty_cell_at(marble.position()); + case 2: + return marble.position() != m_preview_marbles[0].position() && marble.position() != m_preview_marbles[1].position() && is_empty_cell_at(marble.position()); + default: + VERIFY_NOT_REACHED(); + } + }; + if (use_current && is_valid_marble()) { + continue; + } + while (!empty_cells.is_empty()) { + auto const position = empty_cells.take_last(); + Color const new_color = get_random_uniform(Marble::number_of_colors); + marble = Marble { position, new_color }; + if (!is_valid_marble()) + continue; + set_preview_marble(i, marble); + break; + } + if (empty_cells.is_empty()) + return false; + } + return empty_cells.size() > 0; + } + + [[nodiscard]] bool ensure_all_preview_marbles_are_on_empty_cells() + { + if (check_preview_marbles_are_valid()) + return true; + return update_preview_marbles(true); + } + + [[nodiscard]] Color color_at(Point point) const + { + VERIFY(in_bounds(point)); + return m_board[point.y()][point.x()]; + } + + void set_color_at(Point point, Color color) + { + VERIFY(in_bounds(point)); + m_board[point.y()][point.x()] = color; + } + + void clear_color_at(Point point) + { + set_color_at(point, empty_cell); + } + + [[nodiscard]] bool is_empty_cell_at(Point point) const + { + return color_at(point) == empty_cell; + } + + [[nodiscard]] static bool in_bounds(Point point) + { + return point.x() >= 0 && point.x() < board_size.width() && point.y() >= 0 && point.y() < board_size.height(); + } + + [[nodiscard]] bool build_marble_path(Point from, Point to, MarblePath& path) const + { + path.reset(); + + if (from == to || !MarbleBoard::in_bounds(from) || !MarbleBoard::in_bounds(to)) { + return false; + } + + struct Trace { + public: + using Value = u8; + + Trace() { reset(); } + + ~Trace() = default; + + [[nodiscard]] Value operator[](Point point) const + { + return m_map[point.y()][point.x()]; + } + + Value& operator[](Point point) + { + return m_map[point.y()][point.x()]; + } + + void reset() + { + for (size_t y = 0; y < board_size.height(); ++y) + for (size_t x = 0; x < board_size.width(); ++x) + m_map[y][x] = NumericLimits::max(); + } + + private: + BoardMap m_map; + }; + + Trace trace; + trace[from] = 1; + + Queue queue; + queue.enqueue(from); + + auto add_path_point = [&](Point point, u8 value) { + if (MarbleBoard::in_bounds(point) && is_empty_cell_at(point) && trace[point] > value) { + trace[point] = value; + queue.enqueue(point); + } + }; + + constexpr Point connected_four_ways[4] = { + { 0, -1 }, // to the top + { 0, 1 }, // to the bottom + { -1, 0 }, // to the left + { 1, 0 } // to the right + }; + + while (!queue.is_empty()) { + auto current = queue.dequeue(); + if (current == to) { + while (current != from) { + path.add_point(current); + for (auto delta : connected_four_ways) + if (auto next = current.translated(delta); MarbleBoard::in_bounds(next) && trace[next] < trace[current]) { + current = next; + break; + } + } + path.add_point(current); + return true; + } + for (auto delta : connected_four_ways) + add_path_point(current.translated(delta), trace[current] + 1); + } + return false; + } + + [[nodiscard]] bool check_and_remove_marbles() + { + m_removed_marbles.clear(); + constexpr Point connected_four_ways[] = { + { -1, 0 }, // to the left + { 0, -1 }, // to the top + { -1, -1 }, // to the top-left + { 1, -1 } // to the top-right + }; + HashTable> marbles; + for_each_cell([&](Point current_point) { + if (is_empty_cell_at(current_point)) + return IterationDecision::Continue; + auto const color { color_at(current_point) }; + for (auto direction : connected_four_ways) { + size_t marble_count = 0; + for (auto p = current_point; in_bounds(p) && color_at(p) == color; p.translate_by(direction)) + ++marble_count; + if (marble_count >= number_of_marbles_to_remove) + for (auto p = current_point; in_bounds(p) && color_at(p) == color; p.translate_by(direction)) + marbles.set({ p, color }); + } + return IterationDecision::Continue; + }); + m_removed_marbles.ensure_capacity(marbles.size()); + for (auto const& marble : marbles) { + m_removed_marbles.append(marble); + clear_color_at(marble.position()); + } + return !m_removed_marbles.is_empty(); + } + + [[nodiscard]] PreviewMarbles const& preview_marbles() const + { + return m_preview_marbles; + } + + [[nodiscard]] bool has_selected_marble() const + { + return m_selected_marble != nullptr; + } + + [[nodiscard]] SelectedMarble const& selected_marble() const + { + VERIFY(has_selected_marble()); + return *m_selected_marble; + } + + [[nodiscard]] bool select_marble(Point point) + { + if (!is_empty_cell_at(point)) { + m_selected_marble = make(point, color_at(point)); + return true; + } + return false; + } + + void reset_selection() + { + m_selected_marble.clear(); + } + + [[nodiscard]] MarbleArray const& removed_marbles() const + { + return m_removed_marbles; + } + + void reset() + { + reset_selection(); + for (size_t i = 0; i < number_of_preview_marbles; ++i) + m_preview_marbles[i] = { { 0, 0 }, empty_cell }; + m_removed_marbles.clear(); + for_each_cell([&](Point point) { + set_color_at(point, empty_cell); + return IterationDecision::Continue; + }); + } + +private: + static void for_each_cell(Function functor) + { + for (int y = 0; y < board_size.height(); ++y) + for (int x = 0; x < board_size.width(); ++x) + if (functor({ x, y }) == IterationDecision::Break) + return; + } + + [[nodiscard]] bool place_preview_marble_on_board(PreviewMarble const& marble) + { + if (!is_empty_cell_at(marble.position())) + return false; + set_color_at(marble.position(), marble.color()); + return true; + } + + static void random_shuffle(PointArray& points) + { + // Using Fisher–Yates in-place shuffle + if (points.size() > 1) + for (size_t i = points.size() - 1; i > 1; --i) + swap(points[i], points[get_random_uniform(i + 1)]); + } + + static constexpr int number_of_marbles_to_remove { 5 }; + + using Row = Array; + using BoardMap = Array; + + BoardMap m_board; + PreviewMarbles m_preview_marbles; + MarbleArray m_removed_marbles; + OwnPtr m_selected_marble {}; +}; diff --git a/Userland/Games/ColorLines/MarblePath.h b/Userland/Games/ColorLines/MarblePath.h new file mode 100644 index 0000000000..e6d178e988 --- /dev/null +++ b/Userland/Games/ColorLines/MarblePath.h @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2022, Oleg Kosenkov + * Copyright (c) 2022, the SerenityOS developers. + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include +#include + +class MarblePath final { +public: + using Point = Gfx::IntPoint; + + MarblePath() = default; + + void add_point(Point point) + { + m_path.append(point); + } + + [[nodiscard]] bool is_empty() const + { + return m_path.is_empty(); + } + + [[nodiscard]] bool contains(Point point) const + { + return m_path.contains_slow(point); + } + + [[nodiscard]] size_t remaining_steps() const + { + return m_path.size(); + } + + [[nodiscard]] Point current_point() const + { + VERIFY(!m_path.is_empty()); + return m_path.last(); + } + + [[nodiscard]] Point next_point() + { + auto const point = current_point(); + m_path.resize(m_path.size() - 1); + return point; + } + + [[nodiscard]] Point operator[](size_t index) const + { + return m_path[index]; + } + + void reset() + { + m_path.clear(); + } + +private: + Vector m_path; +}; diff --git a/Userland/Games/ColorLines/main.cpp b/Userland/Games/ColorLines/main.cpp new file mode 100644 index 0000000000..163a39ea21 --- /dev/null +++ b/Userland/Games/ColorLines/main.cpp @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2022, Oleg Kosenkov + * Copyright (c) 2022, the SerenityOS developers. + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include "ColorLines.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +ErrorOr serenity_main(Main::Arguments arguments) +{ + TRY(Core::System::pledge("stdio rpath recvfd sendfd unix")); + + auto app = TRY(GUI::Application::try_create(arguments)); + + auto const app_name = "ColorLines"sv; + auto const title = "Color Lines"sv; + auto const man_file = "/usr/share/man/man6/ColorLines.md"sv; + + Config::pledge_domain(app_name); + + TRY(Desktop::Launcher::add_allowed_handler_with_only_specific_urls("/bin/Help", { URL::create_with_file_scheme(man_file) })); + TRY(Desktop::Launcher::seal_allowlist()); + + TRY(Core::System::pledge("stdio rpath recvfd sendfd")); + + TRY(Core::System::unveil("/tmp/session/%sid/portal/launch", "rw")); + TRY(Core::System::unveil("/res", "r")); + TRY(Core::System::unveil(nullptr, nullptr)); + + auto app_icon = TRY(GUI::Icon::try_create_default_icon("app-colorlines"sv)); + + auto window = TRY(GUI::Window::try_create()); + + window->set_double_buffering_enabled(false); + window->set_title(title); + window->resize(436, 481); + window->set_resizable(false); + + auto game = TRY(window->try_set_main_widget(app_name)); + + auto game_menu = TRY(window->try_add_menu("&Game")); + + TRY(game_menu->try_add_action(GUI::Action::create("&New Game", { Mod_None, Key_F2 }, TRY(Gfx::Bitmap::try_load_from_file("/res/icons/16x16/reload.png"sv)), [&](auto&) { + game->reset(); + }))); + TRY(game_menu->try_add_separator()); + TRY(game_menu->try_add_action(GUI::CommonActions::make_quit_action([](auto&) { + GUI::Application::the()->quit(); + }))); + + auto help_menu = TRY(window->try_add_menu("&Help")); + TRY(help_menu->try_add_action(GUI::CommonActions::make_command_palette_action(window))); + TRY(help_menu->try_add_action(GUI::CommonActions::make_help_action([&man_file](auto&) { + Desktop::Launcher::open(URL::create_with_file_scheme(man_file), "/bin/Help"); + }))); + TRY(help_menu->try_add_action(GUI::CommonActions::make_about_action(title, app_icon, window))); + + window->show(); + + window->set_icon(app_icon.bitmap_for_size(16)); + + return app->exec(); +}