diff options
-rw-r--r-- | Base/res/apps/BrickGame.af | 4 | ||||
-rw-r--r-- | Base/res/icons/16x16/app-brickgame.png | bin | 0 -> 592 bytes | |||
-rw-r--r-- | Base/res/icons/32x32/app-brickgame.png | bin | 0 -> 1287 bytes | |||
-rw-r--r-- | Base/usr/share/man/man6/BrickGame.md | 15 | ||||
-rw-r--r-- | Userland/Games/BrickGame/BrickGame.cpp | 590 | ||||
-rw-r--r-- | Userland/Games/BrickGame/BrickGame.h | 46 | ||||
-rw-r--r-- | Userland/Games/BrickGame/CMakeLists.txt | 13 | ||||
-rw-r--r-- | Userland/Games/BrickGame/main.cpp | 76 | ||||
-rw-r--r-- | Userland/Games/CMakeLists.txt | 1 |
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 Binary files differnew file mode 100644 index 0000000000..6e1504b1ad --- /dev/null +++ b/Base/res/icons/16x16/app-brickgame.png diff --git a/Base/res/icons/32x32/app-brickgame.png b/Base/res/icons/32x32/app-brickgame.png Binary files differnew file mode 100644 index 0000000000..084dc1ce61 --- /dev/null +++ b/Base/res/icons/32x32/app-brickgame.png 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) |