/* * Copyright (c) 2022, the SerenityOS developers. * * SPDX-License-Identifier: BSD-2-Clause */ #include "BrickGame.h" #include #include #include #include #include #include #include using Position = Gfx::Point; 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 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 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()) { 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: event.ignore(); 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, DeprecatedString const& text) { auto const text_width = static_cast(ceilf(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, DeprecatedString::formatted("Score: {}", m_brick_game->score())); paint_text(painter, 1, DeprecatedString::formatted("Level: {}", m_brick_game->level())); paint_text(painter, 4, DeprecatedString::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_deprecated_string(), "Game Over"sv, GUI::MessageBox::Type::Information); reset(); }