summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Base/res/apps/BrickGame.af4
-rw-r--r--Base/res/icons/16x16/app-brickgame.pngbin0 -> 592 bytes
-rw-r--r--Base/res/icons/32x32/app-brickgame.pngbin0 -> 1287 bytes
-rw-r--r--Base/usr/share/man/man6/BrickGame.md15
-rw-r--r--Userland/Games/BrickGame/BrickGame.cpp590
-rw-r--r--Userland/Games/BrickGame/BrickGame.h46
-rw-r--r--Userland/Games/BrickGame/CMakeLists.txt13
-rw-r--r--Userland/Games/BrickGame/main.cpp76
-rw-r--r--Userland/Games/CMakeLists.txt1
9 files changed, 745 insertions, 0 deletions
diff --git a/Base/res/apps/BrickGame.af b/Base/res/apps/BrickGame.af
new file mode 100644
index 0000000000..a4a2085af0
--- /dev/null
+++ b/Base/res/apps/BrickGame.af
@@ -0,0 +1,4 @@
+[App]
+Name=Brick Game
+Executable=/bin/BrickGame
+Category=Games
diff --git a/Base/res/icons/16x16/app-brickgame.png b/Base/res/icons/16x16/app-brickgame.png
new file mode 100644
index 0000000000..6e1504b1ad
--- /dev/null
+++ b/Base/res/icons/16x16/app-brickgame.png
Binary files differ
diff --git a/Base/res/icons/32x32/app-brickgame.png b/Base/res/icons/32x32/app-brickgame.png
new file mode 100644
index 0000000000..084dc1ce61
--- /dev/null
+++ b/Base/res/icons/32x32/app-brickgame.png
Binary files differ
diff --git a/Base/usr/share/man/man6/BrickGame.md b/Base/usr/share/man/man6/BrickGame.md
new file mode 100644
index 0000000000..41fb47ed69
--- /dev/null
+++ b/Base/usr/share/man/man6/BrickGame.md
@@ -0,0 +1,15 @@
+## Name
+
+![Icon](/res/icons/16x16/app-brickgame.png) BrickGame
+
+[Open](file:///bin/BrickGame)
+
+## Synopsis
+
+```**sh
+$ BrickGame
+```
+
+## Description
+
+BrickGame is a classic game.
diff --git a/Userland/Games/BrickGame/BrickGame.cpp b/Userland/Games/BrickGame/BrickGame.cpp
new file mode 100644
index 0000000000..0ab704c671
--- /dev/null
+++ b/Userland/Games/BrickGame/BrickGame.cpp
@@ -0,0 +1,590 @@
+/*
+ * Copyright (c) 2022, the SerenityOS developers.
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#include "BrickGame.h"
+#include <AK/Random.h>
+#include <LibConfig/Client.h>
+#include <LibGUI/MessageBox.h>
+#include <LibGUI/Painter.h>
+#include <LibGfx/Bitmap.h>
+#include <LibGfx/Font/FontDatabase.h>
+#include <LibGfx/Point.h>
+
+using Position = Gfx::Point<int>;
+
+class Well final {
+public:
+ using Row = u32;
+
+ Well()
+ {
+ reset();
+ }
+
+ [[nodiscard]] static constexpr size_t number_of_columns() { return 10; }
+
+ [[nodiscard]] static constexpr size_t number_of_rows() { return top_margin() + 18 + bottom_margin(); }
+
+ [[nodiscard]] static constexpr size_t left_margin() { return margin_left; }
+
+ [[nodiscard]] static constexpr size_t top_margin() { return margin_top; }
+
+ [[nodiscard]] static constexpr size_t bottom_margin() { return margin_bottom; }
+
+ [[nodiscard]] Row operator[](size_t i) const { return m_rows[i]; }
+
+ Row& operator[](size_t i) { return m_rows[i]; }
+
+ [[nodiscard]] bool operator[](Position pos) const
+ {
+ return (m_rows[pos.y()] & (1 << (31 - pos.x()))) != 0;
+ }
+
+ void reset()
+ {
+ auto const rows = number_of_rows();
+ for (size_t row_index = 0; row_index < rows; ++row_index)
+ m_rows[row_index] = s_empty_row;
+
+ for (size_t row_index = rows - margin_bottom; row_index < rows; ++row_index)
+ m_rows[row_index] = s_full_row;
+ }
+
+ size_t check_and_remove_full_rows()
+ {
+ size_t number_of_removed_rows {};
+ auto current = int(number_of_rows() - bottom_margin());
+ for (auto row { current - 1 }; row >= 0; --row) {
+ if (m_rows[row] == s_full_row) {
+ number_of_removed_rows += 1;
+ continue;
+ }
+ m_rows[--current] = m_rows[row];
+ }
+ for (; current >= 0; --current)
+ m_rows[current] = s_empty_row;
+ return number_of_removed_rows;
+ }
+
+private:
+ static constexpr size_t column_count = 10;
+ static constexpr size_t row_count = 18;
+
+ static constexpr size_t margin_left = 4;
+ static constexpr size_t margin_top = 1;
+ static constexpr size_t margin_right = 32 - margin_left - column_count;
+ static constexpr size_t margin_bottom = 4;
+
+ static constexpr size_t total_row_count = row_count + margin_top + margin_bottom;
+
+ // empty row looks like 0b1111'0000'0000'0011'1111'1111'1111'1111
+ // note, we have margin on both sides to implement collision checking for
+ // block shapes.
+ static constexpr Row s_empty_row = ~((~(~0u << column_count)) << margin_right);
+
+ // full row looks like 0b1111'1111'1111'1111'1111'1111'1111'1111
+ static constexpr Row s_full_row = ~0;
+
+ // A well is an array of rows, each row has 32 columns, each column is represented as a bit in the u32.
+ // First column has index 0, it is the most significant bit in the u32.
+ // An empty cell in the row is represented as a zero bit.
+ // For convenience of testing of block-wall collisions the well starts at the non-zero margin
+ // from top, left, right and bottom, i.e. it is surrounded with walls of specified width/height (margin).
+ // Note, that block-well collision testing is a simple and fast 'and' bit operation of well row bit contents and
+ // the shape row bits contents.
+ Array<Row, total_row_count> m_rows {}; // 0 is the topmost row in the well
+};
+
+class Block final {
+public:
+ static constexpr size_t shape_size = 4;
+
+ Block() = default;
+ Block(Block const&) = default;
+ Block& operator=(Block const&) = default;
+
+ Block& rotate_left()
+ {
+ m_rotation = m_rotation == 0 ? number_of_rotations - 1 : m_rotation - 1;
+ return *this;
+ }
+
+ Block& rotate_right()
+ {
+ m_rotation = m_rotation == number_of_rotations - 1 ? 0 : m_rotation + 1;
+ return *this;
+ }
+
+ Block& move_left()
+ {
+ m_position = m_position.moved_left(1);
+ return *this;
+ }
+
+ Block& move_right()
+ {
+ m_position = m_position.moved_right(1);
+ return *this;
+ }
+
+ Block& move_down()
+ {
+ m_position = m_position.moved_down(1);
+ return *this;
+ }
+
+ Block& move_to(Position pos)
+ {
+ m_position = pos;
+ return *this;
+ }
+
+ Block& random_shape()
+ {
+ m_shape = get_random_uniform(number_of_shapes);
+ m_rotation = 0;
+ m_position = { 6, 0 };
+ return *this;
+ }
+
+ [[nodiscard]] bool operator[](Position pos) const
+ {
+ return (block_row(pos.y() - m_position.y()) & (1 << (31 - pos.x()))) != 0;
+ }
+
+ [[nodiscard]] bool has_collision(Well const& well) const
+ {
+ for (size_t start_row = 0; start_row < shape_size; ++start_row) {
+ auto const row_index = start_row + m_position.y();
+ if (row_index >= Well::number_of_rows() || (well[row_index] & block_row(start_row)) != 0)
+ return true;
+ }
+ return false;
+ }
+
+ void place_into(Well& well)
+ {
+ for (size_t row_index = 0; row_index < shape_size; ++row_index)
+ well[m_position.y() + row_index] |= block_row(row_index);
+ }
+
+ [[nodiscard]] bool dot_at(Position position) const
+ {
+ return ((shape_data_at(position.y()) & (1 << (3 - position.x()))) != 0);
+ }
+
+private:
+ static constexpr u8 number_of_shapes = 7;
+ static constexpr u8 number_of_rotations = 4;
+
+ using Shape = u16;
+ using Row = u32;
+
+ // Each shape is stored in one u16, each nibble is representing one shape row, highest nibble being the first row.
+ // Every shape has 4x4 dimension and there are 4 possible shape rotations.
+ static constexpr Shape s_shapes[number_of_shapes][number_of_rotations] = {
+ // Shape: I
+ { 0b0000'1111'0000'0000, 0b0010'0010'0010'0010, 0b0000'1111'0000'0000,
+ 0b0010'0010'0010'0010 },
+
+ // Shape: J
+ { 0b0000'0111'0001'0000, 0b0001'0001'0011'0000, 0b0000'0100'0111'0000,
+ 0b0011'0010'0010'0000 },
+
+ // Shape: L
+ { 0b0000'0111'0100'0000, 0b0110'0010'0010'0000, 0b0000'0001'0111'0000,
+ 0b0010'0010'0011'0000 },
+
+ // Shape: O
+ { 0b0000'0110'0110'0000, 0b0000'0110'0110'0000, 0b0000'0110'0110'0000,
+ 0b0000'0110'0110'0000 },
+
+ // Shape: S
+ { 0b0000'0011'0110'0000, 0b0100'0110'0010'0000, 0b0000'0011'0110'0000,
+ 0b0100'0110'0010'0000 },
+
+ // Shape: T
+ { 0b0000'0111'0010'0000, 0b0001'0011'0001'0000, 0b0000'0010'0111'0000,
+ 0b0100'0110'0100'0000 },
+
+ // Shape: Z
+ { 0b0000'0110'0011'0000, 0b0001'0011'0010'0000, 0b0000'0110'0011'0000,
+ 0b0001'0011'0010'0000 }
+ };
+
+ Position m_position {};
+ u8 m_rotation {};
+ u8 m_shape {};
+
+ [[nodiscard]] Row block_row(size_t row) const
+ {
+ return shape_data_at(row) << (32 - m_position.x() - shape_size);
+ }
+
+ [[nodiscard]] Row shape_data_at(size_t row) const
+ {
+ switch (row) {
+ case 0:
+ return Row((s_shapes[m_shape][m_rotation] >> 12) & 0xf);
+ case 1:
+ return Row((s_shapes[m_shape][m_rotation] >> 8) & 0xf);
+ case 2:
+ return Row((s_shapes[m_shape][m_rotation] >> 4) & 0xf);
+ case 3:
+ return Row(s_shapes[m_shape][m_rotation] & 0xf);
+ default:
+ return Row {};
+ }
+ }
+};
+
+class Bricks final {
+
+public:
+ enum class GameState {
+ Active,
+ GameOver
+ };
+
+ // Game will request a UI update when any of these events occur:
+ // - score changes
+ // - level changes
+ // - current block position or rotation changes
+ // - any well row(s) state change
+ enum class RenderRequest {
+ SkipRender,
+ RequestUpdate
+ };
+
+ Bricks() { reset(); }
+
+ [[nodiscard]] unsigned score() const { return m_score; }
+
+ [[nodiscard]] unsigned level() const { return m_level; }
+
+ [[nodiscard]] GameState state() const { return m_state; }
+
+ void add_new_block()
+ {
+ m_block = m_next_block;
+ m_next_block.random_shape();
+ m_state = m_block.has_collision(m_well) ? GameState::GameOver : GameState::Active;
+ }
+
+ [[nodiscard]] Block const& next_block() const { return m_next_block; }
+
+ [[nodiscard]] bool operator[](Position pos) const
+ {
+ return m_well[pos] || m_block[pos];
+ }
+
+ [[nodiscard]] RenderRequest rotate_left() { return set_current_block(Block(m_block).rotate_left()); }
+
+ [[nodiscard]] RenderRequest rotate_right() { return set_current_block(Block(m_block).rotate_right()); }
+
+ [[nodiscard]] RenderRequest move_left() { return set_current_block(Block(m_block).move_left()); }
+
+ [[nodiscard]] RenderRequest move_right() { return set_current_block(Block(m_block).move_right()); }
+
+ [[nodiscard]] RenderRequest move_down()
+ {
+ auto const block = Block(m_block).move_down();
+ if (block.has_collision(m_well)) {
+ m_block.place_into(m_well);
+ check_and_remove_full_rows();
+ add_new_block();
+ return RenderRequest::RequestUpdate;
+ }
+ m_block = block;
+ return RenderRequest::RequestUpdate;
+ }
+
+ RenderRequest move_down_fast()
+ {
+ for (auto block = m_block;; block.move_down()) {
+ if (block.has_collision(m_well)) {
+ m_block.place_into(m_well);
+ check_and_remove_full_rows();
+ add_new_block();
+ break;
+ }
+ m_block = block;
+ }
+ return RenderRequest::RequestUpdate;
+ }
+
+ [[nodiscard]] RenderRequest update()
+ {
+ auto const current_level { m_level };
+ for (size_t i = 0; i < s_level_map.size(); i++) {
+ if (m_score < s_level_map[i].m_score)
+ break;
+ m_level = i;
+ }
+ auto const now { Time::now_realtime() };
+ auto const delay = s_level_map[m_level].m_delay;
+ if (now - m_last_update > delay) {
+ m_last_update = now;
+ return move_down();
+ }
+ return current_level == m_level ? RenderRequest::SkipRender : RenderRequest::RequestUpdate;
+ }
+
+ void reset()
+ {
+ m_level = 0;
+ m_score = 0;
+ m_well.reset();
+ m_block.random_shape();
+ m_next_block.random_shape();
+ m_last_update = Time::now_realtime();
+ m_state = GameState::Active;
+ }
+
+private:
+ Well m_well {};
+ Block m_block {};
+ Block m_next_block {};
+ unsigned m_level {};
+ unsigned m_score {};
+ GameState m_state { GameState::GameOver };
+ Time m_last_update {};
+
+ struct LevelMap final {
+ unsigned const m_score;
+ Time const m_delay;
+ };
+
+ static constexpr Array<LevelMap, 14> s_level_map = {
+ LevelMap { 0, Time::from_milliseconds(38000 / 60) },
+ LevelMap { 1000, Time::from_milliseconds(34000 / 60) },
+ LevelMap { 2000, Time::from_milliseconds(29000 / 60) },
+ LevelMap { 3000, Time::from_milliseconds(25000 / 60) },
+ LevelMap { 4000, Time::from_milliseconds(22000 / 60) },
+ LevelMap { 5000, Time::from_milliseconds(18000 / 60) },
+ LevelMap { 6000, Time::from_milliseconds(15000 / 60) },
+ LevelMap { 7000, Time::from_milliseconds(11000 / 60) },
+ LevelMap { 8000, Time::from_milliseconds(7000 / 60) },
+ LevelMap { 9000, Time::from_milliseconds(5000 / 60) },
+ LevelMap { 10000, Time::from_milliseconds(4000 / 60) },
+ LevelMap { 20000, Time::from_milliseconds(3000 / 60) },
+ LevelMap { 30000, Time::from_milliseconds(2000 / 60) },
+ LevelMap { 10000000, Time::from_milliseconds(1000 / 60) }
+ };
+
+ [[nodiscard]] RenderRequest set_current_block(Block const& block)
+ {
+ if (!block.has_collision(m_well)) {
+ m_block = block;
+ return RenderRequest::RequestUpdate;
+ }
+ return RenderRequest::SkipRender;
+ }
+
+ RenderRequest check_and_remove_full_rows()
+ {
+ auto const number_of_removed_rows { m_well.check_and_remove_full_rows() };
+ switch (number_of_removed_rows) {
+ case 0:
+ return RenderRequest::SkipRender;
+ case 1:
+ m_score += 40 * (m_level + 1);
+ break;
+ case 2:
+ m_score += 100 * (m_level + 1);
+ break;
+ case 3:
+ m_score += 300 * (m_level + 1);
+ break;
+ case 4:
+ m_score += 1200 * (m_level + 1);
+ break;
+ default:
+ VERIFY_NOT_REACHED();
+ }
+ return RenderRequest::RequestUpdate;
+ }
+};
+
+BrickGame::BrickGame(StringView app_name)
+ : m_app_name { app_name }
+ , m_state { GameState::Idle }
+ , m_brick_game(make<Bricks>())
+{
+ 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 BrickGame::reset()
+{
+ m_state = GameState::Active;
+ m_brick_game->reset();
+ stop_timer();
+ start_timer(15); // 66.6ms
+ m_brick_game->add_new_block();
+ // A new game must always succeed to start, otherwise it is not fun to play
+ VERIFY(m_brick_game->state() == Bricks::GameState::Active);
+ update();
+}
+
+void BrickGame::timer_event(Core::TimerEvent&)
+{
+ if (m_brick_game->state() == Bricks::GameState::GameOver) {
+ game_over();
+ return;
+ }
+ if (m_brick_game->update() == Bricks::RenderRequest::RequestUpdate)
+ update();
+}
+
+void BrickGame::keydown_event(GUI::KeyEvent& event)
+{
+ Bricks::RenderRequest render_request { Bricks::RenderRequest::SkipRender };
+ switch (event.key()) {
+ case KeyCode::Key_A:
+ case KeyCode::Key_H:
+ case KeyCode::Key_Left:
+ render_request = m_brick_game->move_left();
+ break;
+ case KeyCode::Key_D:
+ case KeyCode::Key_L:
+ case KeyCode::Key_Right:
+ render_request = m_brick_game->move_right();
+ break;
+ case KeyCode::Key_W:
+ case KeyCode::Key_K:
+ case KeyCode::Key_Up:
+ render_request = m_brick_game->rotate_right();
+ break;
+ case KeyCode::Key_E:
+ render_request = m_brick_game->rotate_left();
+ break;
+ case KeyCode::Key_S:
+ case KeyCode::Key_Down:
+ render_request = m_brick_game->move_down();
+ break;
+ case KeyCode::Key_Space:
+ render_request = m_brick_game->move_down_fast();
+ break;
+ default:
+ break;
+ }
+ if (render_request == Bricks::RenderRequest::RequestUpdate)
+ update();
+}
+
+void BrickGame::paint_cell(GUI::Painter& painter, Gfx::IntRect rect, bool is_on)
+{
+ painter.draw_rect(rect, m_back_color);
+ rect.inflate(-1, -1, -1, -1);
+ painter.draw_rect(rect, is_on ? m_front_color : m_shadow_color);
+ painter.set_pixel(rect.top_left(), m_back_color);
+ painter.set_pixel(rect.bottom_left(), m_back_color);
+ painter.set_pixel(rect.top_right(), m_back_color);
+ painter.set_pixel(rect.bottom_right(), m_back_color);
+ rect.inflate(-2, -2);
+ painter.draw_rect(rect, is_on ? m_front_color : m_shadow_color);
+ rect.inflate(-2, -2);
+ painter.draw_rect(rect, m_back_color);
+ rect.inflate(-2, -2);
+ painter.draw_rect(rect, m_back_color);
+ rect.inflate(-2, -2);
+ painter.fill_rect(rect, is_on ? m_front_color : m_shadow_color);
+}
+
+void BrickGame::paint_text(GUI::Painter& painter, int row, String const& text)
+{
+ auto const text_width { font().width(text) };
+ auto const entire_area_rect { frame_inner_rect() };
+ auto const margin = 4;
+ auto const glyph_height = font().glyph_height();
+ auto const rect { Gfx::IntRect { entire_area_rect.x() + entire_area_rect.width() - 116,
+ 2 * margin + entire_area_rect.y() + (glyph_height + margin) * row,
+ text_width, glyph_height } };
+ painter.draw_text(rect, text, Gfx::TextAlignment::TopLeft, Color::Black);
+}
+
+void BrickGame::paint_game(GUI::Painter& painter, Gfx::IntRect const& rect)
+{
+ painter.fill_rect(rect, m_back_color);
+ if (m_state == GameState::Active) {
+ // TODO: optimize repainting
+ painter.draw_rect(rect.inflated(-4, -4), m_front_color);
+
+ auto const entire_area_rect { frame_inner_rect() };
+ Gfx::IntRect well_rect { entire_area_rect };
+ well_rect.inflate(0, -120, 0, 0);
+ well_rect.inflate(-4, -4);
+ painter.draw_rect(well_rect, m_front_color);
+ well_rect.inflate(-4, -4);
+
+ auto const cell_size = Gfx::IntSize(well_rect.width() / Well::number_of_columns(), well_rect.height() / (Well::number_of_rows() - Well::top_margin() - Well::bottom_margin()));
+ auto cell_rect = [&](Position pos) {
+ return Gfx::IntRect {
+ well_rect.x() + pos.x() * cell_size.width(),
+ well_rect.y() + pos.y() * cell_size.height(),
+ cell_size.width() - 1,
+ cell_size.height() - 1
+ };
+ };
+
+ auto const number_of_columns = int(Well::number_of_columns());
+ auto const number_of_rows = int(Well::number_of_rows() - Well::top_margin() - Well::bottom_margin());
+ for (int row = 0; row < number_of_rows; ++row)
+ for (int col = 0; col < number_of_columns; ++col) {
+ auto const position = Position { col, row };
+ auto const board_position = position.translated(int(Well::left_margin()), int(Well::top_margin()));
+ paint_cell(painter, cell_rect(position), (*m_brick_game)[board_position]);
+ }
+
+ paint_text(painter, 0, String::formatted("Score: {}", m_brick_game->score()));
+ paint_text(painter, 1, String::formatted("Level: {}", m_brick_game->level()));
+ paint_text(painter, 4, String::formatted("Hi-Score: {}", m_high_score));
+ paint_text(painter, 12, "Next:");
+
+ auto const hint_rect = Gfx::IntRect {
+ frame_inner_rect().x() + frame_inner_rect().width() - 105,
+ frame_inner_rect().y() + 200,
+ int(cell_size.width() * Block::shape_size),
+ int(cell_size.height() * Block::shape_size)
+ };
+
+ painter.draw_rect(hint_rect.inflated(4, 4), m_front_color);
+
+ auto const dot_rect = Gfx::IntRect { hint_rect.x(), hint_rect.y(), cell_size.width() - 1, cell_size.height() - 1 };
+ for (size_t y = 0; y < Block::shape_size; ++y)
+ for (size_t x = 0; x < Block::shape_size; ++x)
+ paint_cell(painter, dot_rect.translated(int(x * cell_size.width()), int(y * cell_size.height())), m_brick_game->next_block().dot_at({ x, y }));
+ }
+}
+
+void BrickGame::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());
+ paint_game(painter, frame_inner_rect());
+}
+
+void BrickGame::game_over()
+{
+ stop_timer();
+ StringBuilder text;
+ auto const current_score = m_brick_game->score();
+ text.appendff("Your score was {}", current_score);
+ if (current_score > m_high_score) {
+ text.append("\nThat's a new high score!"sv);
+ Config::write_i32(m_app_name, m_app_name, "HighScore"sv, int(m_high_score = current_score));
+ }
+ GUI::MessageBox::show(window(),
+ text.to_string(),
+ "Game Over"sv,
+ GUI::MessageBox::Type::Information);
+
+ reset();
+}
diff --git a/Userland/Games/BrickGame/BrickGame.h b/Userland/Games/BrickGame/BrickGame.h
new file mode 100644
index 0000000000..202d8898f0
--- /dev/null
+++ b/Userland/Games/BrickGame/BrickGame.h
@@ -0,0 +1,46 @@
+/*
+ * Copyright (c) 2022, the SerenityOS developers.
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#pragma once
+
+#include <AK/NonnullOwnPtr.h>
+#include <LibGUI/Frame.h>
+
+class Bricks;
+
+class BrickGame : public GUI::Frame {
+ C_OBJECT(BrickGame);
+
+public:
+ virtual ~BrickGame() override = default;
+
+ void reset();
+
+private:
+ BrickGame(StringView app_name);
+ virtual void paint_event(GUI::PaintEvent&) override;
+ virtual void keydown_event(GUI::KeyEvent&) override;
+ virtual void timer_event(Core::TimerEvent&) override;
+
+ void paint_text(GUI::Painter&, int row, String const&);
+ void paint_cell(GUI::Painter&, Gfx::IntRect, bool);
+ void paint_game(GUI::Painter&, Gfx::IntRect const&);
+ void game_over();
+
+ enum class GameState {
+ Idle = 0,
+ Active
+ };
+
+ StringView const m_app_name;
+ GameState m_state {};
+ NonnullOwnPtr<Bricks> m_brick_game;
+ unsigned m_high_score {};
+
+ Color m_back_color { Color::from_rgb(0x8fbc8f) };
+ Color m_front_color { Color::Black };
+ Color m_shadow_color { Color::from_rgb(0x729672) };
+};
diff --git a/Userland/Games/BrickGame/CMakeLists.txt b/Userland/Games/BrickGame/CMakeLists.txt
new file mode 100644
index 0000000000..a016f0a30d
--- /dev/null
+++ b/Userland/Games/BrickGame/CMakeLists.txt
@@ -0,0 +1,13 @@
+serenity_component(
+ BrickGame
+ RECOMMENDED
+ TARGETS BrickGame
+)
+
+set(SOURCES
+ main.cpp
+ BrickGame.cpp
+)
+
+serenity_app(BrickGame ICON app-brickgame)
+target_link_libraries(BrickGame PRIVATE LibGUI LibCore LibGfx LibConfig LibMain LibDesktop)
diff --git a/Userland/Games/BrickGame/main.cpp b/Userland/Games/BrickGame/main.cpp
new file mode 100644
index 0000000000..28576493f6
--- /dev/null
+++ b/Userland/Games/BrickGame/main.cpp
@@ -0,0 +1,76 @@
+/*
+ * Copyright (c) 2022, the SerenityOS developers.
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#include "BrickGame.h"
+#include <AK/URL.h>
+#include <LibConfig/Client.h>
+#include <LibCore/System.h>
+#include <LibDesktop/Launcher.h>
+#include <LibGUI/Action.h>
+#include <LibGUI/Application.h>
+#include <LibGUI/BoxLayout.h>
+#include <LibGUI/Button.h>
+#include <LibGUI/Icon.h>
+#include <LibGUI/Menu.h>
+#include <LibGUI/Menubar.h>
+#include <LibGUI/Window.h>
+#include <LibMain/Main.h>
+
+ErrorOr<int> 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 = "BrickGame"sv;
+ auto const title = "Brick Game"sv;
+ auto const man_file = "/usr/share/man/man6/BrickGame.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-brickgame"sv));
+
+ auto window = TRY(GUI::Window::try_create());
+
+ window->set_double_buffering_enabled(false);
+ window->set_title(title);
+ window->resize(360, 462);
+ window->set_resizable(false);
+
+ auto game = TRY(window->try_set_main_widget<BrickGame>(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();
+}
diff --git a/Userland/Games/CMakeLists.txt b/Userland/Games/CMakeLists.txt
index e04e3f28a0..8a7aae6b5b 100644
--- a/Userland/Games/CMakeLists.txt
+++ b/Userland/Games/CMakeLists.txt
@@ -1,4 +1,5 @@
add_subdirectory(2048)
+add_subdirectory(BrickGame)
add_subdirectory(Chess)
add_subdirectory(FlappyBug)
add_subdirectory(Flood)