Serenity Operating System
at master 301 lines 8.0 kB view raw
1/* 2 * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org> 3 * Copyright (c) 2021, Mustafa Quraish <mustafa@serenityos.org> 4 * Copyright (c) 2022, the SerenityOS developers. 5 * 6 * SPDX-License-Identifier: BSD-2-Clause 7 */ 8 9#include "Game.h" 10#include <AK/Random.h> 11#include <LibConfig/Client.h> 12#include <LibGUI/MessageBox.h> 13#include <LibGUI/Painter.h> 14#include <LibGfx/Bitmap.h> 15#include <LibGfx/Font/Font.h> 16#include <LibGfx/Font/FontDatabase.h> 17 18REGISTER_WIDGET(Snake, Game); 19 20namespace Snake { 21 22ErrorOr<NonnullRefPtr<Game>> Game::try_create() 23{ 24 static constexpr auto food_bitmaps_files = Array { 25 "/res/emoji/U+1F41F.png"sv, 26 "/res/emoji/U+1F95A.png"sv, 27 "/res/emoji/U+1F99C.png"sv, 28 "/res/emoji/U+1F986.png"sv, 29 "/res/emoji/U+1FAB2.png"sv, 30 "/res/emoji/U+1F426.png"sv, 31 "/res/emoji/U+1F424.png"sv, 32 "/res/emoji/U+1F40D.png"sv, 33 "/res/emoji/U+1F989.png"sv, 34 "/res/emoji/U+1F54A.png"sv, 35 "/res/emoji/U+1F408.png"sv, 36 "/res/emoji/U+1F420.png"sv, 37 "/res/emoji/U+1F415.png"sv, 38 "/res/emoji/U+1F429.png"sv, 39 "/res/emoji/U+1F98C.png"sv, 40 "/res/emoji/U+1F416.png"sv, 41 "/res/emoji/U+1F401.png"sv, 42 "/res/emoji/U+1F400.png"sv, 43 "/res/emoji/U+1F407.png"sv, 44 "/res/emoji/U+1F43F.png"sv, 45 "/res/emoji/U+1F9A5.png"sv, 46 "/res/emoji/U+1F423.png"sv, 47 "/res/emoji/U+1F425.png"sv, 48 "/res/emoji/U+1F98E.png"sv, 49 "/res/emoji/U+1F997.png"sv, 50 "/res/emoji/U+1FAB3.png"sv, 51 "/res/emoji/U+1F413.png"sv, 52 "/res/emoji/U+1FAB0.png"sv, 53 "/res/emoji/U+1FAB1.png"sv, 54 }; 55 56 Vector<NonnullRefPtr<Gfx::Bitmap>> food_bitmaps; 57 TRY(food_bitmaps.try_ensure_capacity(food_bitmaps_files.size())); 58 59 for (auto file : food_bitmaps_files) { 60 auto bitmap = Gfx::Bitmap::load_from_file(file); 61 if (bitmap.is_error()) { 62 dbgln("\033[31;1mCould not load bitmap file\033[0m '{}': {}", file, bitmap.error()); 63 return bitmap.release_error(); 64 } 65 66 food_bitmaps.unchecked_append(bitmap.release_value()); 67 } 68 69 return adopt_nonnull_ref_or_enomem(new (nothrow) Game(move(food_bitmaps))); 70} 71 72Game::Game(Vector<NonnullRefPtr<Gfx::Bitmap>> food_bitmaps) 73 : m_food_bitmaps(move(food_bitmaps)) 74{ 75 set_font(Gfx::FontDatabase::default_fixed_width_font().bold_variant()); 76 reset(); 77 78 m_snake_base_color = Color::from_argb(Config::read_u32("Snake"sv, "Snake"sv, "BaseColor"sv, m_snake_base_color.value())); 79} 80 81void Game::pause() 82{ 83 stop_timer(); 84} 85 86void Game::start() 87{ 88 static constexpr int timer_ms = 100; 89 start_timer(timer_ms); 90} 91 92void Game::reset() 93{ 94 m_head = { m_rows / 2, m_columns / 2 }; 95 m_tail.clear_with_capacity(); 96 m_length = 2; 97 m_score = 0; 98 m_is_new_high_score = false; 99 m_velocity_queue.clear(); 100 101 if (on_score_update) 102 on_score_update(m_score); 103 104 pause(); 105 start(); 106 spawn_fruit(); 107 update(); 108} 109 110void Game::set_snake_base_color(Color color) 111{ 112 Config::write_u32("Snake"sv, "Snake"sv, "BaseColor"sv, color.value()); 113 m_snake_base_color = color; 114} 115 116bool Game::is_available(Coordinate const& coord) 117{ 118 for (size_t i = 0; i < m_tail.size(); ++i) { 119 if (m_tail[i] == coord) 120 return false; 121 } 122 if (m_head == coord) 123 return false; 124 if (m_fruit == coord) 125 return false; 126 return true; 127} 128 129void Game::spawn_fruit() 130{ 131 Coordinate coord; 132 for (;;) { 133 coord.row = get_random_uniform(m_rows); 134 coord.column = get_random_uniform(m_columns); 135 if (is_available(coord)) 136 break; 137 } 138 m_fruit = coord; 139 m_fruit_type = get_random_uniform(m_food_bitmaps.size()); 140} 141 142void Game::timer_event(Core::TimerEvent&) 143{ 144 Vector<Coordinate> dirty_cells; 145 146 m_tail.prepend(m_head); 147 148 if (m_tail.size() > m_length) { 149 dirty_cells.append(m_tail.last()); 150 m_tail.take_last(); 151 } 152 153 if (!m_velocity_queue.is_empty()) 154 m_velocity = m_velocity_queue.dequeue(); 155 156 dirty_cells.append(m_head); 157 158 m_head.row += m_velocity.vertical; 159 m_head.column += m_velocity.horizontal; 160 161 m_last_velocity = m_velocity; 162 163 if (m_head.row >= m_rows) 164 m_head.row = 0; 165 if (m_head.row < 0) 166 m_head.row = m_rows - 1; 167 if (m_head.column >= m_columns) 168 m_head.column = 0; 169 if (m_head.column < 0) 170 m_head.column = m_columns - 1; 171 172 dirty_cells.append(m_head); 173 174 for (size_t i = 0; i < m_tail.size(); ++i) { 175 if (m_head == m_tail[i]) { 176 game_over(); 177 return; 178 } 179 } 180 181 if (m_head == m_fruit) { 182 ++m_length; 183 ++m_score; 184 185 if (on_score_update) 186 m_is_new_high_score = on_score_update(m_score); 187 188 dirty_cells.append(m_fruit); 189 spawn_fruit(); 190 dirty_cells.append(m_fruit); 191 } 192 193 for (auto& coord : dirty_cells) { 194 update(cell_rect(coord)); 195 } 196} 197 198void Game::keydown_event(GUI::KeyEvent& event) 199{ 200 switch (event.key()) { 201 case KeyCode::Key_A: 202 case KeyCode::Key_Left: 203 if (last_velocity().horizontal == 1) 204 break; 205 queue_velocity(0, -1); 206 break; 207 case KeyCode::Key_D: 208 case KeyCode::Key_Right: 209 if (last_velocity().horizontal == -1) 210 break; 211 queue_velocity(0, 1); 212 break; 213 case KeyCode::Key_W: 214 case KeyCode::Key_Up: 215 if (last_velocity().vertical == 1) 216 break; 217 queue_velocity(-1, 0); 218 break; 219 case KeyCode::Key_S: 220 case KeyCode::Key_Down: 221 if (last_velocity().vertical == -1) 222 break; 223 queue_velocity(1, 0); 224 break; 225 default: 226 event.ignore(); 227 break; 228 } 229} 230 231Gfx::IntRect Game::cell_rect(Coordinate const& coord) const 232{ 233 auto game_rect = frame_inner_rect(); 234 auto cell_size = Gfx::IntSize(game_rect.width() / m_columns, game_rect.height() / m_rows); 235 return { 236 game_rect.x() + coord.column * cell_size.width(), 237 game_rect.y() + coord.row * cell_size.height(), 238 cell_size.width(), 239 cell_size.height() 240 }; 241} 242 243void Game::paint_event(GUI::PaintEvent& event) 244{ 245 GUI::Frame::paint_event(event); 246 GUI::Painter painter(*this); 247 painter.add_clip_rect(frame_inner_rect()); 248 painter.add_clip_rect(event.rect()); 249 painter.fill_rect(event.rect(), Color::Black); 250 251 painter.fill_rect(cell_rect(m_head), m_snake_base_color); 252 for (auto& part : m_tail) { 253 auto rect = cell_rect(part); 254 painter.fill_rect(rect, m_snake_base_color.darkened(0.77)); 255 256 Gfx::IntRect left_side(rect.x(), rect.y(), 2, rect.height()); 257 Gfx::IntRect top_side(rect.x(), rect.y(), rect.width(), 2); 258 Gfx::IntRect right_side(rect.right() - 1, rect.y(), 2, rect.height()); 259 Gfx::IntRect bottom_side(rect.x(), rect.bottom() - 1, rect.width(), 2); 260 painter.fill_rect(left_side, m_snake_base_color.darkened(0.88)); 261 painter.fill_rect(right_side, m_snake_base_color.darkened(0.55)); 262 painter.fill_rect(top_side, m_snake_base_color.darkened(0.88)); 263 painter.fill_rect(bottom_side, m_snake_base_color.darkened(0.55)); 264 } 265 266 painter.draw_scaled_bitmap(cell_rect(m_fruit), m_food_bitmaps[m_fruit_type], m_food_bitmaps[m_fruit_type]->rect()); 267} 268 269void Game::game_over() 270{ 271 stop_timer(); 272 273 StringBuilder text; 274 text.appendff("Your score was {}", m_score); 275 if (m_is_new_high_score) { 276 text.append("\nThat's a new high score!"sv); 277 } 278 GUI::MessageBox::show(window(), 279 text.to_deprecated_string(), 280 "Game Over"sv, 281 GUI::MessageBox::Type::Information); 282 283 reset(); 284} 285 286void Game::queue_velocity(int v, int h) 287{ 288 if (last_velocity().vertical == v && last_velocity().horizontal == h) 289 return; 290 m_velocity_queue.enqueue({ v, h }); 291} 292 293Game::Velocity const& Game::last_velocity() const 294{ 295 if (!m_velocity_queue.is_empty()) 296 return m_velocity_queue.last(); 297 298 return m_last_velocity; 299} 300 301}