Games: Add BrickGame

This commit is contained in:
Oleg Kosenkov 2022-11-01 10:36:51 -04:00 committed by Andrew Kaster
parent 73f4cfa930
commit 46c6176fad
Notes: sideshowbarker 2024-07-19 17:02:18 +09:00
9 changed files with 745 additions and 0 deletions

View file

@ -0,0 +1,4 @@
[App]
Name=Brick Game
Executable=/bin/BrickGame
Category=Games

Binary file not shown.

After

Width:  |  Height:  |  Size: 592 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

View file

@ -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.

View file

@ -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();
}

View file

@ -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) };
};

View file

@ -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)

View file

@ -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();
}

View file

@ -1,4 +1,5 @@
add_subdirectory(2048)
add_subdirectory(BrickGame)
add_subdirectory(Chess)
add_subdirectory(FlappyBug)
add_subdirectory(Flood)