summaryrefslogtreecommitdiff
path: root/Userland/Games
diff options
context:
space:
mode:
Diffstat (limited to 'Userland/Games')
-rw-r--r--Userland/Games/2048/BoardView.cpp215
-rw-r--r--Userland/Games/2048/BoardView.h61
-rw-r--r--Userland/Games/2048/CMakeLists.txt9
-rw-r--r--Userland/Games/2048/Game.cpp229
-rw-r--r--Userland/Games/2048/Game.h78
-rw-r--r--Userland/Games/2048/GameSizeDialog.cpp95
-rw-r--r--Userland/Games/2048/GameSizeDialog.h45
-rw-r--r--Userland/Games/2048/main.cpp215
-rw-r--r--Userland/Games/Breakout/CMakeLists.txt8
-rw-r--r--Userland/Games/Breakout/Game.cpp328
-rw-r--r--Userland/Games/Breakout/Game.h109
-rw-r--r--Userland/Games/Breakout/LevelSelectDialog.cpp82
-rw-r--r--Userland/Games/Breakout/LevelSelectDialog.h45
-rw-r--r--Userland/Games/Breakout/main.cpp90
-rw-r--r--Userland/Games/CMakeLists.txt8
-rw-r--r--Userland/Games/Chess/CMakeLists.txt9
-rw-r--r--Userland/Games/Chess/ChessWidget.cpp659
-rw-r--r--Userland/Games/Chess/ChessWidget.h147
-rw-r--r--Userland/Games/Chess/Engine.cpp88
-rw-r--r--Userland/Games/Chess/Engine.h58
-rw-r--r--Userland/Games/Chess/PromotionDialog.cpp59
-rw-r--r--Userland/Games/Chess/PromotionDialog.h42
-rw-r--r--Userland/Games/Chess/main.cpp231
-rw-r--r--Userland/Games/Conway/CMakeLists.txt7
-rw-r--r--Userland/Games/Conway/Game.cpp120
-rw-r--r--Userland/Games/Conway/Game.h52
-rw-r--r--Userland/Games/Conway/main.cpp90
-rw-r--r--Userland/Games/Minesweeper/CMakeLists.txt7
-rw-r--r--Userland/Games/Minesweeper/Field.cpp541
-rw-r--r--Userland/Games/Minesweeper/Field.h132
-rw-r--r--Userland/Games/Minesweeper/main.cpp152
-rw-r--r--Userland/Games/Pong/CMakeLists.txt7
-rw-r--r--Userland/Games/Pong/Game.cpp236
-rw-r--r--Userland/Games/Pong/Game.h120
-rw-r--r--Userland/Games/Pong/main.cpp90
-rw-r--r--Userland/Games/Snake/CMakeLists.txt7
-rw-r--r--Userland/Games/Snake/SnakeGame.cpp257
-rw-r--r--Userland/Games/Snake/SnakeGame.h91
-rw-r--r--Userland/Games/Snake/main.cpp102
-rw-r--r--Userland/Games/Solitaire/CMakeLists.txt9
-rw-r--r--Userland/Games/Solitaire/Card.cpp177
-rw-r--r--Userland/Games/Solitaire/Card.h85
-rw-r--r--Userland/Games/Solitaire/CardStack.cpp238
-rw-r--r--Userland/Games/Solitaire/CardStack.h100
-rw-r--r--Userland/Games/Solitaire/SolitaireWidget.cpp446
-rw-r--r--Userland/Games/Solitaire/SolitaireWidget.h142
-rw-r--r--Userland/Games/Solitaire/main.cpp85
47 files changed, 6203 insertions, 0 deletions
diff --git a/Userland/Games/2048/BoardView.cpp b/Userland/Games/2048/BoardView.cpp
new file mode 100644
index 0000000000..230c1eedc5
--- /dev/null
+++ b/Userland/Games/2048/BoardView.cpp
@@ -0,0 +1,215 @@
+/*
+ * Copyright (c) 2020, the SerenityOS developers.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "BoardView.h"
+#include <LibGUI/Painter.h>
+#include <LibGfx/Font.h>
+#include <LibGfx/FontDatabase.h>
+#include <LibGfx/Palette.h>
+
+BoardView::BoardView(const Game::Board* board)
+ : m_board(board)
+{
+}
+
+BoardView::~BoardView()
+{
+}
+
+void BoardView::set_board(const Game::Board* board)
+{
+ if (m_board == board)
+ return;
+
+ if (!board) {
+ m_board = nullptr;
+ return;
+ }
+
+ bool must_resize = !m_board || m_board->size() != board->size();
+
+ m_board = board;
+
+ if (must_resize)
+ resize();
+
+ update();
+}
+
+void BoardView::pick_font()
+{
+ String best_font_name;
+ int best_font_size = -1;
+ auto& font_database = Gfx::FontDatabase::the();
+ font_database.for_each_font([&](const Gfx::Font& font) {
+ if (font.family() != "Liza" || font.weight() != 700)
+ return;
+ auto size = font.glyph_height();
+ if (size * 2 <= m_cell_size && size > best_font_size) {
+ best_font_name = font.qualified_name();
+ best_font_size = size;
+ }
+ });
+
+ auto font = font_database.get_by_name(best_font_name);
+ set_font(font);
+}
+
+size_t BoardView::rows() const
+{
+ if (!m_board)
+ return 0;
+ return m_board->size();
+}
+
+size_t BoardView::columns() const
+{
+ if (!m_board)
+ return 0;
+ if (m_board->is_empty())
+ return 0;
+ return (*m_board)[0].size();
+}
+
+void BoardView::resize_event(GUI::ResizeEvent&)
+{
+ resize();
+}
+
+void BoardView::resize()
+{
+ constexpr float padding_ratio = 7;
+ m_padding = min(
+ width() / (columns() * (padding_ratio + 1) + 1),
+ height() / (rows() * (padding_ratio + 1) + 1));
+ m_cell_size = m_padding * padding_ratio;
+
+ pick_font();
+}
+
+void BoardView::keydown_event(GUI::KeyEvent& event)
+{
+ if (!on_move)
+ return;
+
+ switch (event.key()) {
+ case KeyCode::Key_A:
+ case KeyCode::Key_Left:
+ on_move(Game::Direction::Left);
+ break;
+ case KeyCode::Key_D:
+ case KeyCode::Key_Right:
+ on_move(Game::Direction::Right);
+ break;
+ case KeyCode::Key_W:
+ case KeyCode::Key_Up:
+ on_move(Game::Direction::Up);
+ break;
+ case KeyCode::Key_S:
+ case KeyCode::Key_Down:
+ on_move(Game::Direction::Down);
+ break;
+ default:
+ return;
+ }
+}
+
+Gfx::Color BoardView::background_color_for_cell(u32 value)
+{
+ switch (value) {
+ case 0:
+ return Color::from_rgb(0xcdc1b4);
+ case 2:
+ return Color::from_rgb(0xeee4da);
+ case 4:
+ return Color::from_rgb(0xede0c8);
+ case 8:
+ return Color::from_rgb(0xf2b179);
+ case 16:
+ return Color::from_rgb(0xf59563);
+ case 32:
+ return Color::from_rgb(0xf67c5f);
+ case 64:
+ return Color::from_rgb(0xf65e3b);
+ case 128:
+ return Color::from_rgb(0xedcf72);
+ case 256:
+ return Color::from_rgb(0xedcc61);
+ case 512:
+ return Color::from_rgb(0xedc850);
+ case 1024:
+ return Color::from_rgb(0xedc53f);
+ case 2048:
+ return Color::from_rgb(0xedc22e);
+ default:
+ ASSERT(value > 2048);
+ return Color::from_rgb(0x3c3a32);
+ }
+}
+
+Gfx::Color BoardView::text_color_for_cell(u32 value)
+{
+ if (value <= 4)
+ return Color::from_rgb(0x776e65);
+ return Color::from_rgb(0xf9f6f2);
+}
+
+void BoardView::paint_event(GUI::PaintEvent&)
+{
+ Color background_color = Color::from_rgb(0xbbada0);
+
+ GUI::Painter painter(*this);
+
+ if (!m_board) {
+ painter.fill_rect(rect(), background_color);
+ return;
+ }
+ auto& board = *m_board;
+
+ Gfx::IntRect field_rect {
+ 0,
+ 0,
+ static_cast<int>(m_padding + (m_cell_size + m_padding) * columns()),
+ static_cast<int>(m_padding + (m_cell_size + m_padding) * rows())
+ };
+ field_rect.center_within(rect());
+ painter.fill_rect(field_rect, background_color);
+
+ for (size_t column = 0; column < columns(); ++column) {
+ for (size_t row = 0; row < rows(); ++row) {
+ auto rect = Gfx::IntRect {
+ field_rect.x() + m_padding + (m_cell_size + m_padding) * column,
+ field_rect.y() + m_padding + (m_cell_size + m_padding) * row,
+ m_cell_size,
+ m_cell_size,
+ };
+ auto entry = board[row][column];
+ painter.fill_rect(rect, background_color_for_cell(entry));
+ if (entry > 0)
+ painter.draw_text(rect, String::number(entry), font(), Gfx::TextAlignment::Center, text_color_for_cell(entry));
+ }
+ }
+}
diff --git a/Userland/Games/2048/BoardView.h b/Userland/Games/2048/BoardView.h
new file mode 100644
index 0000000000..d5a0679d1a
--- /dev/null
+++ b/Userland/Games/2048/BoardView.h
@@ -0,0 +1,61 @@
+/*
+ * Copyright (c) 2020, the SerenityOS developers.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include "Game.h"
+#include <LibGUI/Widget.h>
+
+class BoardView final : public GUI::Widget {
+ C_OBJECT(BoardView)
+
+public:
+ BoardView(const Game::Board*);
+ virtual ~BoardView() override;
+
+ void set_board(const Game::Board* board);
+
+ Function<void(Game::Direction)> on_move;
+
+private:
+ virtual void resize_event(GUI::ResizeEvent&) override;
+ virtual void paint_event(GUI::PaintEvent&) override;
+ virtual void keydown_event(GUI::KeyEvent&) override;
+
+ size_t rows() const;
+ size_t columns() const;
+
+ void pick_font();
+ void resize();
+
+ Color background_color_for_cell(u32 value);
+ Color text_color_for_cell(u32 value);
+
+ float m_padding { 0 };
+ float m_cell_size { 0 };
+
+ const Game::Board* m_board { nullptr };
+};
diff --git a/Userland/Games/2048/CMakeLists.txt b/Userland/Games/2048/CMakeLists.txt
new file mode 100644
index 0000000000..d48b1e7bcf
--- /dev/null
+++ b/Userland/Games/2048/CMakeLists.txt
@@ -0,0 +1,9 @@
+set(SOURCES
+ BoardView.cpp
+ Game.cpp
+ GameSizeDialog.cpp
+ main.cpp
+)
+
+serenity_app(2048 ICON app-2048)
+target_link_libraries(2048 LibGUI)
diff --git a/Userland/Games/2048/Game.cpp b/Userland/Games/2048/Game.cpp
new file mode 100644
index 0000000000..6b3bd7b2bb
--- /dev/null
+++ b/Userland/Games/2048/Game.cpp
@@ -0,0 +1,229 @@
+/*
+ * Copyright (c) 2020, the SerenityOS developers.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "Game.h"
+#include <AK/String.h>
+#include <stdlib.h>
+
+Game::Game(size_t grid_size, size_t target_tile)
+ : m_grid_size(grid_size)
+{
+ if (target_tile == 0)
+ m_target_tile = 2048;
+ else if ((target_tile & (target_tile - 1)) != 0)
+ m_target_tile = 1 << max_power_for_board(grid_size);
+ else
+ m_target_tile = target_tile;
+
+ m_board.resize(grid_size);
+ for (auto& row : m_board) {
+ row.ensure_capacity(grid_size);
+ for (size_t i = 0; i < grid_size; i++)
+ row.append(0);
+ }
+
+ add_random_tile();
+ add_random_tile();
+}
+
+void Game::add_random_tile()
+{
+ int row;
+ int column;
+ do {
+ row = rand() % m_grid_size;
+ column = rand() % m_grid_size;
+ } while (m_board[row][column] != 0);
+
+ size_t value = rand() < RAND_MAX * 0.9 ? 2 : 4;
+ m_board[row][column] = value;
+}
+
+static Game::Board transpose(const Game::Board& board)
+{
+ Vector<Vector<u32>> new_board;
+ auto result_row_count = board[0].size();
+ auto result_column_count = board.size();
+
+ new_board.resize(result_row_count);
+
+ for (size_t i = 0; i < board.size(); ++i) {
+ auto& row = new_board[i];
+ row.clear_with_capacity();
+ row.ensure_capacity(result_column_count);
+ for (auto& entry : board) {
+ row.append(entry[i]);
+ }
+ }
+
+ return new_board;
+}
+
+static Game::Board reverse(const Game::Board& board)
+{
+ auto new_board = board;
+ for (auto& row : new_board) {
+ for (size_t i = 0; i < row.size() / 2; ++i)
+ swap(row[i], row[row.size() - i - 1]);
+ }
+
+ return new_board;
+}
+
+static Vector<u32> slide_row(const Vector<u32>& row, size_t& successful_merge_score)
+{
+ if (row.size() < 2)
+ return row;
+
+ auto x = row[0];
+ auto y = row[1];
+
+ auto result = row;
+ result.take_first();
+
+ if (x == 0) {
+ result = slide_row(result, successful_merge_score);
+ result.append(0);
+ return result;
+ }
+
+ if (y == 0) {
+ result[0] = x;
+ result = slide_row(result, successful_merge_score);
+ result.append(0);
+ return result;
+ }
+
+ if (x == y) {
+ result.take_first();
+ result = slide_row(result, successful_merge_score);
+ result.append(0);
+ result.prepend(x + x);
+ successful_merge_score += x * 2;
+ return result;
+ }
+
+ result = slide_row(result, successful_merge_score);
+ result.prepend(x);
+ return result;
+}
+
+static Game::Board slide_left(const Game::Board& board, size_t& successful_merge_score)
+{
+ Vector<Vector<u32>> new_board;
+ for (auto& row : board)
+ new_board.append(slide_row(row, successful_merge_score));
+
+ return new_board;
+}
+
+static bool is_complete(const Game::Board& board, size_t target)
+{
+ for (auto& row : board) {
+ if (row.contains_slow(target))
+ return true;
+ }
+
+ return false;
+}
+
+static bool has_no_neighbors(const Span<const u32>& row)
+{
+ if (row.size() < 2)
+ return true;
+
+ auto x = row[0];
+ auto y = row[1];
+
+ if (x == y)
+ return false;
+
+ return has_no_neighbors(row.slice(1, row.size() - 1));
+};
+
+static bool is_stalled(const Game::Board& board)
+{
+ static auto stalled = [](auto& row) {
+ return !row.contains_slow(0) && has_no_neighbors(row.span());
+ };
+
+ for (auto& row : board)
+ if (!stalled(row))
+ return false;
+
+ for (auto& row : transpose(board))
+ if (!stalled(row))
+ return false;
+
+ return true;
+}
+
+Game::MoveOutcome Game::attempt_move(Direction direction)
+{
+ size_t successful_merge_score = 0;
+ Board new_board;
+
+ switch (direction) {
+ case Direction::Left:
+ new_board = slide_left(m_board, successful_merge_score);
+ break;
+ case Direction::Right:
+ new_board = reverse(slide_left(reverse(m_board), successful_merge_score));
+ break;
+ case Direction::Up:
+ new_board = transpose(slide_left(transpose(m_board), successful_merge_score));
+ break;
+ case Direction::Down:
+ new_board = transpose(reverse(slide_left(reverse(transpose(m_board)), successful_merge_score)));
+ break;
+ }
+
+ bool moved = new_board != m_board;
+ if (moved) {
+ m_board = new_board;
+ m_turns++;
+ add_random_tile();
+ m_score += successful_merge_score;
+ }
+
+ if (is_complete(m_board, m_target_tile))
+ return MoveOutcome::Won;
+ if (is_stalled(m_board))
+ return MoveOutcome::GameOver;
+ if (moved)
+ return MoveOutcome::OK;
+ return MoveOutcome::InvalidMove;
+}
+
+u32 Game::largest_tile() const
+{
+ u32 tile = 0;
+ for (auto& row : board()) {
+ for (auto& cell : row)
+ tile = max(tile, cell);
+ }
+ return tile;
+}
diff --git a/Userland/Games/2048/Game.h b/Userland/Games/2048/Game.h
new file mode 100644
index 0000000000..570dae671c
--- /dev/null
+++ b/Userland/Games/2048/Game.h
@@ -0,0 +1,78 @@
+/*
+ * Copyright (c) 2020, the SerenityOS developers.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include <AK/Vector.h>
+
+class Game final {
+public:
+ Game(size_t board_size, size_t target_tile = 0);
+ Game(const Game&) = default;
+
+ enum class MoveOutcome {
+ OK,
+ InvalidMove,
+ GameOver,
+ Won,
+ };
+
+ enum class Direction {
+ Up,
+ Down,
+ Left,
+ Right,
+ };
+
+ MoveOutcome attempt_move(Direction);
+
+ size_t score() const { return m_score; }
+ size_t turns() const { return m_turns; }
+ u32 target_tile() const { return m_target_tile; }
+ u32 largest_tile() const;
+
+ using Board = Vector<Vector<u32>>;
+
+ const Board& board() const { return m_board; }
+
+ static size_t max_power_for_board(size_t size)
+ {
+ if (size >= 6)
+ return 31;
+
+ return size * size + 1;
+ }
+
+private:
+ void add_random_tile();
+
+ size_t m_grid_size { 0 };
+ u32 m_target_tile { 0 };
+
+ Board m_board;
+ size_t m_score { 0 };
+ size_t m_turns { 0 };
+};
diff --git a/Userland/Games/2048/GameSizeDialog.cpp b/Userland/Games/2048/GameSizeDialog.cpp
new file mode 100644
index 0000000000..a9ef824351
--- /dev/null
+++ b/Userland/Games/2048/GameSizeDialog.cpp
@@ -0,0 +1,95 @@
+/*
+ * Copyright (c) 2020, the SerenityOS developers.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "GameSizeDialog.h"
+#include "Game.h"
+#include <LibGUI/BoxLayout.h>
+#include <LibGUI/Button.h>
+#include <LibGUI/CheckBox.h>
+#include <LibGUI/Label.h>
+#include <LibGUI/SpinBox.h>
+
+GameSizeDialog::GameSizeDialog(GUI::Window* parent)
+ : GUI::Dialog(parent)
+{
+ set_rect({ 0, 0, 200, 150 });
+ set_title("New Game");
+ set_icon(parent->icon());
+ set_resizable(false);
+
+ auto& main_widget = set_main_widget<GUI::Widget>();
+ main_widget.set_fill_with_background_color(true);
+
+ auto& layout = main_widget.set_layout<GUI::VerticalBoxLayout>();
+ layout.set_margins({ 4, 4, 4, 4 });
+
+ auto& board_size_box = main_widget.add<GUI::Widget>();
+ auto& input_layout = board_size_box.set_layout<GUI::HorizontalBoxLayout>();
+ input_layout.set_spacing(4);
+
+ board_size_box.add<GUI::Label>("Board size").set_text_alignment(Gfx::TextAlignment::CenterLeft);
+ auto& spinbox = board_size_box.add<GUI::SpinBox>();
+
+ auto& target_box = main_widget.add<GUI::Widget>();
+ auto& target_layout = target_box.set_layout<GUI::HorizontalBoxLayout>();
+ target_layout.set_spacing(4);
+ spinbox.set_min(2);
+ spinbox.set_value(m_board_size);
+
+ target_box.add<GUI::Label>("Target tile").set_text_alignment(Gfx::TextAlignment::CenterLeft);
+ auto& tile_value_label = target_box.add<GUI::Label>(String::number(target_tile()));
+ tile_value_label.set_text_alignment(Gfx::TextAlignment::CenterRight);
+ auto& target_spinbox = target_box.add<GUI::SpinBox>();
+ target_spinbox.set_max(Game::max_power_for_board(m_board_size));
+ target_spinbox.set_min(3);
+ target_spinbox.set_value(m_target_tile_power);
+
+ spinbox.on_change = [&](auto value) {
+ m_board_size = value;
+ target_spinbox.set_max(Game::max_power_for_board(m_board_size));
+ };
+
+ target_spinbox.on_change = [&](auto value) {
+ m_target_tile_power = value;
+ tile_value_label.set_text(String::number(target_tile()));
+ };
+
+ auto& temp_checkbox = main_widget.add<GUI::CheckBox>("Temporary");
+ temp_checkbox.set_checked(m_temporary);
+ temp_checkbox.on_checked = [this](auto checked) { m_temporary = checked; };
+
+ auto& buttonbox = main_widget.add<GUI::Widget>();
+ auto& button_layout = buttonbox.set_layout<GUI::HorizontalBoxLayout>();
+ button_layout.set_spacing(10);
+
+ buttonbox.add<GUI::Button>("Cancel").on_click = [this](auto) {
+ done(Dialog::ExecCancel);
+ };
+
+ buttonbox.add<GUI::Button>("OK").on_click = [this](auto) {
+ done(Dialog::ExecOK);
+ };
+}
diff --git a/Userland/Games/2048/GameSizeDialog.h b/Userland/Games/2048/GameSizeDialog.h
new file mode 100644
index 0000000000..ba6122bc81
--- /dev/null
+++ b/Userland/Games/2048/GameSizeDialog.h
@@ -0,0 +1,45 @@
+/*
+ * Copyright (c) 2020, the SerenityOS developers.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include <AK/Types.h>
+#include <LibGUI/Dialog.h>
+
+class GameSizeDialog : public GUI::Dialog {
+ C_OBJECT(GameSizeDialog)
+public:
+ GameSizeDialog(GUI::Window* parent);
+
+ size_t board_size() const { return m_board_size; }
+ u32 target_tile() const { return 1u << m_target_tile_power; }
+ bool temporary() const { return m_temporary; }
+
+private:
+ size_t m_board_size { 4 };
+ size_t m_target_tile_power { 11 };
+ bool m_temporary { true };
+};
diff --git a/Userland/Games/2048/main.cpp b/Userland/Games/2048/main.cpp
new file mode 100644
index 0000000000..e7bae59139
--- /dev/null
+++ b/Userland/Games/2048/main.cpp
@@ -0,0 +1,215 @@
+/*
+ * Copyright (c) 2020, the SerenityOS developers.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "BoardView.h"
+#include "Game.h"
+#include "GameSizeDialog.h"
+#include <LibCore/ConfigFile.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/MessageBox.h>
+#include <LibGUI/StatusBar.h>
+#include <LibGUI/Window.h>
+#include <stdio.h>
+#include <time.h>
+
+int main(int argc, char** argv)
+{
+ if (pledge("stdio rpath wpath cpath shared_buffer accept cpath unix fattr", nullptr) < 0) {
+ perror("pledge");
+ return 1;
+ }
+
+ srand(time(nullptr));
+
+ auto app = GUI::Application::construct(argc, argv);
+ auto app_icon = GUI::Icon::default_icon("app-2048");
+
+ auto window = GUI::Window::construct();
+
+ auto config = Core::ConfigFile::get_for_app("2048");
+
+ size_t board_size = config->read_num_entry("", "board_size", 4);
+ u32 target_tile = config->read_num_entry("", "target_tile", 0);
+
+ config->write_num_entry("", "board_size", board_size);
+ config->write_num_entry("", "target_tile", target_tile);
+
+ config->sync();
+
+ if (pledge("stdio rpath shared_buffer wpath cpath accept", nullptr) < 0) {
+ perror("pledge");
+ return 1;
+ }
+
+ if (unveil("/res", "r") < 0) {
+ perror("unveil");
+ return 1;
+ }
+
+ if (unveil(config->file_name().characters(), "crw") < 0) {
+ perror("unveil");
+ return 1;
+ }
+
+ if (unveil(nullptr, nullptr) < 0) {
+ perror("unveil");
+ return 1;
+ }
+
+ window->set_double_buffering_enabled(false);
+ window->set_title("2048");
+ window->resize(315, 336);
+
+ auto& main_widget = window->set_main_widget<GUI::Widget>();
+ main_widget.set_layout<GUI::VerticalBoxLayout>();
+ main_widget.set_fill_with_background_color(true);
+
+ Game game { board_size, target_tile };
+
+ auto& board_view = main_widget.add<BoardView>(&game.board());
+ board_view.set_focus(true);
+ auto& statusbar = main_widget.add<GUI::StatusBar>();
+
+ auto update = [&]() {
+ board_view.set_board(&game.board());
+ board_view.update();
+ statusbar.set_text(String::formatted("Score: {}", game.score()));
+ };
+
+ update();
+
+ Vector<Game> undo_stack;
+
+ auto change_settings = [&] {
+ auto size_dialog = GameSizeDialog::construct(window);
+ if (size_dialog->exec() || size_dialog->result() != GUI::Dialog::ExecOK)
+ return;
+
+ board_size = size_dialog->board_size();
+ target_tile = size_dialog->target_tile();
+
+ if (!size_dialog->temporary()) {
+
+ config->write_num_entry("", "board_size", board_size);
+ config->write_num_entry("", "target_tile", target_tile);
+
+ if (!config->sync()) {
+ GUI::MessageBox::show(window, "Configuration could not be synced", "Error", GUI::MessageBox::Type::Error);
+ return;
+ }
+ GUI::MessageBox::show(window, "New settings have been saved and will be applied on a new game", "Settings Changed Successfully", GUI::MessageBox::Type::Information);
+ return;
+ }
+
+ GUI::MessageBox::show(window, "New settings have been set and will be applied on the next game", "Settings Changed Successfully", GUI::MessageBox::Type::Information);
+ };
+ auto start_a_new_game = [&] {
+ // Do not leak game states between games.
+ undo_stack.clear();
+
+ game = Game(board_size, target_tile);
+
+ // This ensures that the sizes are correct.
+ board_view.set_board(nullptr);
+ board_view.set_board(&game.board());
+
+ update();
+ window->update();
+ };
+
+ board_view.on_move = [&](Game::Direction direction) {
+ undo_stack.append(game);
+ auto outcome = game.attempt_move(direction);
+ switch (outcome) {
+ case Game::MoveOutcome::OK:
+ if (undo_stack.size() >= 16)
+ undo_stack.take_first();
+ update();
+ break;
+ case Game::MoveOutcome::InvalidMove:
+ undo_stack.take_last();
+ break;
+ case Game::MoveOutcome::Won:
+ update();
+ GUI::MessageBox::show(window,
+ String::formatted("You reached {} in {} turns with a score of {}", game.target_tile(), game.turns(), game.score()),
+ "You won!",
+ GUI::MessageBox::Type::Information);
+ start_a_new_game();
+ break;
+ case Game::MoveOutcome::GameOver:
+ update();
+ GUI::MessageBox::show(window,
+ String::formatted("You reached {} in {} turns with a score of {}", game.largest_tile(), game.turns(), game.score()),
+ "You lost!",
+ GUI::MessageBox::Type::Information);
+ start_a_new_game();
+ break;
+ }
+ };
+
+ auto menubar = GUI::MenuBar::construct();
+
+ auto& app_menu = menubar->add_menu("2048");
+
+ app_menu.add_action(GUI::Action::create("New game", { Mod_None, Key_F2 }, [&](auto&) {
+ start_a_new_game();
+ }));
+
+ app_menu.add_action(GUI::CommonActions::make_undo_action([&](auto&) {
+ if (undo_stack.is_empty())
+ return;
+ game = undo_stack.take_last();
+ update();
+ }));
+
+ app_menu.add_separator();
+
+ app_menu.add_action(GUI::Action::create("Settings", [&](auto&) {
+ change_settings();
+ }));
+
+ app_menu.add_action(GUI::CommonActions::make_quit_action([](auto&) {
+ GUI::Application::the()->quit();
+ }));
+
+ auto& help_menu = menubar->add_menu("Help");
+ help_menu.add_action(GUI::CommonActions::make_about_action("2048", app_icon, window));
+
+ app->set_menubar(move(menubar));
+
+ window->show();
+
+ window->set_icon(app_icon.bitmap_for_size(16));
+
+ return app->exec();
+}
diff --git a/Userland/Games/Breakout/CMakeLists.txt b/Userland/Games/Breakout/CMakeLists.txt
new file mode 100644
index 0000000000..d26284ea20
--- /dev/null
+++ b/Userland/Games/Breakout/CMakeLists.txt
@@ -0,0 +1,8 @@
+set(SOURCES
+ main.cpp
+ Game.cpp
+ LevelSelectDialog.cpp
+)
+
+serenity_app(Breakout ICON app-breakout)
+target_link_libraries(Breakout LibGUI)
diff --git a/Userland/Games/Breakout/Game.cpp b/Userland/Games/Breakout/Game.cpp
new file mode 100644
index 0000000000..c66f45d2b4
--- /dev/null
+++ b/Userland/Games/Breakout/Game.cpp
@@ -0,0 +1,328 @@
+/*
+ * Copyright (c) 2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "Game.h"
+#include "LevelSelectDialog.h"
+#include <LibGUI/Application.h>
+#include <LibGUI/MessageBox.h>
+#include <LibGUI/Painter.h>
+#include <LibGfx/Font.h>
+#include <LibGfx/StandardCursor.h>
+
+namespace Breakout {
+
+Game::Game()
+{
+ set_override_cursor(Gfx::StandardCursor::Hidden);
+ auto level_dialog = LevelSelectDialog::show(m_board, window());
+ if (level_dialog != GUI::Dialog::ExecOK)
+ m_board = -1;
+ set_paused(false);
+ start_timer(16);
+ reset();
+}
+
+Game::~Game()
+{
+}
+
+void Game::reset_paddle()
+{
+ m_paddle.moving_left = false;
+ m_paddle.moving_right = false;
+ m_paddle.rect = { game_width / 2 - 40, game_height - 20, 80, 16 };
+}
+
+void Game::reset()
+{
+ m_lives = 3;
+ m_pause_count = 0;
+ m_cheater = false;
+ reset_ball();
+ reset_paddle();
+ generate_bricks();
+}
+
+void Game::generate_bricks()
+{
+ m_bricks = {};
+
+ Gfx::Color colors[] = {
+ Gfx::Color::Red,
+ Gfx::Color::Green,
+ Gfx::Color::Blue,
+ Gfx::Color::Yellow,
+ Gfx::Color::Magenta,
+ Gfx::Color::Cyan,
+ Gfx::Color::LightGray,
+ };
+
+ Vector<Brick> boards[] = {
+ // :^)
+ Vector({
+ Brick(0, 0, colors[3], 40, 12, 100),
+ Brick(0, 4, colors[3], 40, 12, 100),
+ Brick(1, 2, colors[3], 40, 12, 100),
+ Brick(1, 5, colors[3], 40, 12, 100),
+ Brick(2, 1, colors[3], 40, 12, 100),
+ Brick(2, 3, colors[3], 40, 12, 100),
+ Brick(2, 6, colors[3], 40, 12, 100),
+ Brick(3, 6, colors[3], 40, 12, 100),
+ Brick(4, 0, colors[3], 40, 12, 100),
+ Brick(4, 6, colors[3], 40, 12, 100),
+ Brick(5, 6, colors[3], 40, 12, 100),
+ Brick(6, 5, colors[3], 40, 12, 100),
+ Brick(7, 4, colors[3], 40, 12, 100),
+ })
+ };
+
+ if (m_board != -1) {
+ m_bricks = boards[m_board];
+ } else {
+ // Rainbow
+ for (int row = 0; row < 7; ++row) {
+ for (int column = 0; column < 10; ++column) {
+ Brick brick(row, column, colors[row]);
+ m_bricks.append(brick);
+ }
+ }
+ }
+}
+
+void Game::set_paused(bool paused)
+{
+ m_paused = paused;
+
+ if (m_paused) {
+ set_override_cursor(Gfx::StandardCursor::None);
+ m_pause_count++;
+ } else {
+ set_override_cursor(Gfx::StandardCursor::Hidden);
+ }
+
+ update();
+}
+
+void Game::timer_event(Core::TimerEvent&)
+{
+ if (m_paused)
+ return;
+ tick();
+}
+
+void Game::paint_event(GUI::PaintEvent& event)
+{
+ GUI::Painter painter(*this);
+ painter.add_clip_rect(event.rect());
+
+ painter.fill_rect(rect(), Color::Black);
+
+ painter.fill_ellipse(enclosing_int_rect(m_ball.rect()), Color::Red);
+
+ painter.fill_rect(enclosing_int_rect(m_paddle.rect), Color::White);
+
+ for (auto& brick : m_bricks) {
+ if (!brick.dead)
+ painter.fill_rect(enclosing_int_rect(brick.rect), brick.color);
+ }
+
+ int msg_width = font().width(String::formatted("Lives: {}", m_lives));
+ int msg_height = font().glyph_height();
+ painter.draw_text({ (game_width - msg_width - 2), 2, msg_width, msg_height }, String::formatted("Lives: {}", m_lives), Gfx::TextAlignment::Center, Color::White);
+
+ if (m_paused) {
+ const char* msg = m_cheater ? "C H E A T E R" : "P A U S E D";
+ int msg_width = font().width(msg);
+ int msg_height = font().glyph_height();
+ painter.draw_text({ (game_width / 2) - (msg_width / 2), (game_height / 2) - (msg_height / 2), msg_width, msg_height }, msg, Gfx::TextAlignment::Center, Color::White);
+ }
+}
+
+void Game::keyup_event(GUI::KeyEvent& event)
+{
+ if (m_paused)
+ return;
+ switch (event.key()) {
+ case Key_Left:
+ m_paddle.moving_left = false;
+ break;
+ case Key_Right:
+ m_paddle.moving_right = false;
+ break;
+ default:
+ break;
+ }
+}
+
+void Game::keydown_event(GUI::KeyEvent& event)
+{
+ if (m_paused)
+ return;
+ switch (event.key()) {
+ case Key_Escape:
+ GUI::Application::the()->quit();
+ break;
+ case Key_Left:
+ m_paddle.moving_left = true;
+ break;
+ case Key_Right:
+ m_paddle.moving_right = true;
+ break;
+ default:
+ break;
+ }
+}
+
+void Game::mousemove_event(GUI::MouseEvent& event)
+{
+ if (m_paused)
+ return;
+ float new_paddle_x = event.x() - m_paddle.rect.width() / 2;
+ new_paddle_x = max(0.0f, new_paddle_x);
+ new_paddle_x = min(game_width - m_paddle.rect.width(), new_paddle_x);
+ m_paddle.rect.set_x(new_paddle_x);
+}
+
+void Game::reset_ball()
+{
+ int position_x_min = (game_width / 2) - 50;
+ int position_x_max = (game_width / 2) + 50;
+ int position_x = arc4random() % (position_x_max - position_x_min + 1) + position_x_min;
+ int position_y = 200;
+ int velocity_x = arc4random() % 3 + 1;
+ int velocity_y = 3 + (3 - velocity_x);
+ if (arc4random() % 2)
+ velocity_x = velocity_x * -1;
+
+ m_ball = {};
+ m_ball.position = { position_x, position_y };
+ m_ball.velocity = { velocity_x, velocity_y };
+}
+
+void Game::hurt()
+{
+ stop_timer();
+ m_lives--;
+ if (m_lives <= 0) {
+ update();
+ GUI::MessageBox::show(window(), "You lose!", "Breakout", GUI::MessageBox::Type::Information, GUI::MessageBox::InputType::OK);
+ reset();
+ }
+ sleep(1);
+ reset_ball();
+ reset_paddle();
+ start_timer(16);
+}
+
+void Game::win()
+{
+ stop_timer();
+ update();
+ if (m_cheater) {
+ GUI::MessageBox::show(window(), "You cheated not only the game, but yourself.", "Breakout", GUI::MessageBox::Type::Information, GUI::MessageBox::InputType::OK);
+ } else {
+ GUI::MessageBox::show(window(), "You win!", "Breakout", GUI::MessageBox::Type::Information, GUI::MessageBox::InputType::OK);
+ }
+ reset();
+ start_timer(16);
+}
+
+void Game::tick()
+{
+ auto new_ball = m_ball;
+ new_ball.position += new_ball.velocity;
+
+ if (new_ball.x() < new_ball.radius || new_ball.x() > game_width - new_ball.radius) {
+ new_ball.position.set_x(m_ball.x());
+ new_ball.velocity.set_x(new_ball.velocity.x() * -1);
+ }
+
+ if (new_ball.y() < new_ball.radius) {
+ new_ball.position.set_y(m_ball.y());
+ new_ball.velocity.set_y(new_ball.velocity.y() * -1);
+ }
+
+ if (new_ball.y() > game_height - new_ball.radius) {
+ hurt();
+ return;
+ }
+
+ if (new_ball.rect().intersects(m_paddle.rect)) {
+ new_ball.position.set_y(m_ball.y());
+ new_ball.velocity.set_y(new_ball.velocity.y() * -1);
+
+ float distance_to_middle_of_paddle = new_ball.x() - m_paddle.rect.center().x();
+ float relative_impact_point = distance_to_middle_of_paddle / m_paddle.rect.width();
+ new_ball.velocity.set_x(relative_impact_point * 7);
+ }
+
+ for (auto& brick : m_bricks) {
+ if (brick.dead)
+ continue;
+ if (new_ball.rect().intersects(brick.rect)) {
+ brick.dead = true;
+
+ auto overlap = new_ball.rect().intersected(brick.rect);
+ if (overlap.width() < overlap.height()) {
+ new_ball.position.set_x(m_ball.x());
+ new_ball.velocity.set_x(new_ball.velocity.x() * -1);
+ } else {
+ new_ball.position.set_y(m_ball.y());
+ new_ball.velocity.set_y(new_ball.velocity.y() * -1);
+ }
+ break;
+ }
+ }
+
+ bool has_live_bricks = false;
+ for (auto& brick : m_bricks) {
+ if (!brick.dead) {
+ has_live_bricks = true;
+ break;
+ }
+ }
+
+ if (!has_live_bricks) {
+ win();
+ return;
+ }
+
+ if (m_paddle.moving_left) {
+ m_paddle.rect.set_x(max(0.0f, m_paddle.rect.x() - m_paddle.speed));
+ }
+ if (m_paddle.moving_right) {
+ m_paddle.rect.set_x(min(game_width - m_paddle.rect.width(), m_paddle.rect.x() + m_paddle.speed));
+ }
+
+ m_ball = new_ball;
+
+ if (m_pause_count > 50)
+ m_cheater = true;
+
+ update();
+}
+
+}
diff --git a/Userland/Games/Breakout/Game.h b/Userland/Games/Breakout/Game.h
new file mode 100644
index 0000000000..ffc4675fbc
--- /dev/null
+++ b/Userland/Games/Breakout/Game.h
@@ -0,0 +1,109 @@
+/*
+ * Copyright (c) 2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include <LibGUI/Widget.h>
+
+namespace Breakout {
+
+class Game final : public GUI::Widget {
+ C_OBJECT(Game);
+
+public:
+ static const int game_width = 480;
+ static const int game_height = 500;
+
+ virtual ~Game() override;
+
+ void set_paused(bool paused);
+
+private:
+ Game();
+
+ virtual void paint_event(GUI::PaintEvent&) override;
+ virtual void keyup_event(GUI::KeyEvent&) override;
+ virtual void keydown_event(GUI::KeyEvent&) override;
+ virtual void mousemove_event(GUI::MouseEvent&) override;
+ virtual void timer_event(Core::TimerEvent&) override;
+
+ void reset();
+ void reset_ball();
+ void reset_paddle();
+ void generate_bricks();
+ void tick();
+ void hurt();
+ void win();
+
+ struct Ball {
+ Gfx::FloatPoint position;
+ Gfx::FloatPoint velocity;
+ float radius { 8 };
+
+ float x() const { return position.x(); }
+ float y() const { return position.y(); }
+
+ Gfx::FloatRect rect() const
+ {
+ return { x() - radius, y() - radius, radius * 2, radius * 2 };
+ }
+ };
+
+ struct Paddle {
+ Gfx::FloatRect rect;
+ float speed { 5 };
+ bool moving_left { false };
+ bool moving_right { false };
+ };
+
+ struct Brick {
+ Gfx::FloatRect rect;
+ Gfx::Color color;
+ bool dead { false };
+
+ Brick(int row, int column, Gfx::Color c, int brick_width = 40, int brick_height = 12, int field_left_offset = 30, int field_top_offset = 30, int brick_spacing = 3)
+ {
+ rect = {
+ field_left_offset + (column * brick_width) + (column * brick_spacing),
+ field_top_offset + (row * brick_height) + (row * brick_spacing),
+ brick_width,
+ brick_height
+ };
+ color = c;
+ }
+ };
+
+ bool m_paused;
+ int m_lives;
+ int m_board;
+ long m_pause_count;
+ bool m_cheater;
+ Ball m_ball;
+ Paddle m_paddle;
+ Vector<Brick> m_bricks;
+};
+
+}
diff --git a/Userland/Games/Breakout/LevelSelectDialog.cpp b/Userland/Games/Breakout/LevelSelectDialog.cpp
new file mode 100644
index 0000000000..3836d0de33
--- /dev/null
+++ b/Userland/Games/Breakout/LevelSelectDialog.cpp
@@ -0,0 +1,82 @@
+/*
+ * Copyright (c) 2020, the SerenityOS developers.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "LevelSelectDialog.h"
+#include <LibGUI/BoxLayout.h>
+#include <LibGUI/Button.h>
+#include <LibGUI/Label.h>
+#include <LibGUI/ListView.h>
+
+namespace Breakout {
+
+LevelSelectDialog::LevelSelectDialog(Window* parent_window)
+ : Dialog(parent_window)
+{
+ set_rect(0, 0, 300, 250);
+ set_title("Level Select");
+ build();
+}
+
+LevelSelectDialog::~LevelSelectDialog()
+{
+}
+
+int LevelSelectDialog::show(int& board_number, Window* parent_window)
+{
+ auto box = LevelSelectDialog::construct(parent_window);
+ box->set_resizable(false);
+ if (parent_window)
+ box->set_icon(parent_window->icon());
+ auto result = box->exec();
+ board_number = box->level();
+ return result;
+}
+
+void LevelSelectDialog::build()
+{
+ auto& main_widget = set_main_widget<GUI::Widget>();
+ main_widget.set_fill_with_background_color(true);
+
+ auto& layout = main_widget.set_layout<GUI::VerticalBoxLayout>();
+ layout.set_margins({ 4, 4, 4, 4 });
+
+ main_widget.add<GUI::Label>("Choose a level").set_text_alignment(Gfx::TextAlignment::Center);
+
+ auto& level_list = main_widget.add<GUI::Widget>();
+ auto& scroll_layout = level_list.set_layout<GUI::VerticalBoxLayout>();
+ scroll_layout.set_spacing(4);
+
+ level_list.add<GUI::Button>("Rainbow").on_click = [this](auto) {
+ m_level = -1;
+ done(Dialog::ExecOK);
+ };
+
+ level_list.add<GUI::Button>(":^)").on_click = [this](auto) {
+ m_level = 0;
+ done(Dialog::ExecOK);
+ };
+}
+}
diff --git a/Userland/Games/Breakout/LevelSelectDialog.h b/Userland/Games/Breakout/LevelSelectDialog.h
new file mode 100644
index 0000000000..3cc4d3b6cd
--- /dev/null
+++ b/Userland/Games/Breakout/LevelSelectDialog.h
@@ -0,0 +1,45 @@
+/*
+ * Copyright (c) 2020, the SerenityOS developers.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include <LibGUI/Dialog.h>
+
+namespace Breakout {
+class LevelSelectDialog : public GUI::Dialog {
+ C_OBJECT(LevelSelectDialog)
+public:
+ virtual ~LevelSelectDialog() override;
+ static int show(int& board_number, Window* parent_window);
+ int level() const { return m_level; }
+
+private:
+ explicit LevelSelectDialog(Window* parent_window);
+ void build();
+
+ int m_level;
+};
+}
diff --git a/Userland/Games/Breakout/main.cpp b/Userland/Games/Breakout/main.cpp
new file mode 100644
index 0000000000..e4685cde40
--- /dev/null
+++ b/Userland/Games/Breakout/main.cpp
@@ -0,0 +1,90 @@
+/*
+ * Copyright (c) 2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "Game.h"
+#include <LibGUI/Application.h>
+#include <LibGUI/Icon.h>
+#include <LibGUI/Menu.h>
+#include <LibGUI/MenuBar.h>
+#include <LibGUI/Window.h>
+#include <LibGfx/Bitmap.h>
+
+int main(int argc, char** argv)
+{
+ if (pledge("stdio rpath wpath cpath shared_buffer accept unix fattr", nullptr) < 0) {
+ perror("pledge");
+ return 1;
+ }
+
+ auto app = GUI::Application::construct(argc, argv);
+
+ if (pledge("stdio rpath shared_buffer", nullptr) < 0) {
+ perror("pledge");
+ return 1;
+ }
+
+ if (unveil("/res", "r") < 0) {
+ perror("unveil");
+ return 1;
+ }
+
+ if (unveil(nullptr, nullptr) < 0) {
+ perror("unveil");
+ return 1;
+ }
+
+ auto window = GUI::Window::construct();
+ window->resize(Breakout::Game::game_width, Breakout::Game::game_height);
+ window->set_resizable(false);
+ window->set_double_buffering_enabled(false);
+ window->set_title("Breakout");
+ auto app_icon = GUI::Icon::default_icon("app-breakout");
+ window->set_icon(app_icon.bitmap_for_size(16));
+ auto& game = window->set_main_widget<Breakout::Game>();
+ window->show();
+
+ auto menubar = GUI::MenuBar::construct();
+
+ auto& app_menu = menubar->add_menu("Breakout");
+ app_menu.add_action(GUI::Action::create_checkable("Pause", { {}, Key_P }, [&](auto& action) {
+ game.set_paused(action.is_checked());
+ return;
+ }));
+
+ app_menu.add_separator();
+
+ app_menu.add_action(GUI::CommonActions::make_quit_action([](auto&) {
+ GUI::Application::the()->quit();
+ return;
+ }));
+
+ auto& help_menu = menubar->add_menu("Help");
+ help_menu.add_action(GUI::CommonActions::make_about_action("Breakout", app_icon, window));
+
+ app->set_menubar(move(menubar));
+
+ return app->exec();
+}
diff --git a/Userland/Games/CMakeLists.txt b/Userland/Games/CMakeLists.txt
new file mode 100644
index 0000000000..ea879ed24c
--- /dev/null
+++ b/Userland/Games/CMakeLists.txt
@@ -0,0 +1,8 @@
+add_subdirectory(2048)
+add_subdirectory(Breakout)
+add_subdirectory(Chess)
+add_subdirectory(Conway)
+add_subdirectory(Minesweeper)
+add_subdirectory(Pong)
+add_subdirectory(Snake)
+add_subdirectory(Solitaire)
diff --git a/Userland/Games/Chess/CMakeLists.txt b/Userland/Games/Chess/CMakeLists.txt
new file mode 100644
index 0000000000..54415eeb95
--- /dev/null
+++ b/Userland/Games/Chess/CMakeLists.txt
@@ -0,0 +1,9 @@
+set(SOURCES
+ main.cpp
+ ChessWidget.cpp
+ PromotionDialog.cpp
+ Engine.cpp
+)
+
+serenity_app(Chess ICON app-chess)
+target_link_libraries(Chess LibChess LibGUI LibCore)
diff --git a/Userland/Games/Chess/ChessWidget.cpp b/Userland/Games/Chess/ChessWidget.cpp
new file mode 100644
index 0000000000..6d74a5da20
--- /dev/null
+++ b/Userland/Games/Chess/ChessWidget.cpp
@@ -0,0 +1,659 @@
+/*
+ * Copyright (c) 2020, the SerenityOS developers.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "ChessWidget.h"
+#include "PromotionDialog.h"
+#include <AK/String.h>
+#include <LibCore/DateTime.h>
+#include <LibCore/File.h>
+#include <LibGUI/MessageBox.h>
+#include <LibGUI/Painter.h>
+#include <LibGfx/Font.h>
+#include <LibGfx/FontDatabase.h>
+#include <LibGfx/Path.h>
+#include <unistd.h>
+
+ChessWidget::ChessWidget(const StringView& set)
+{
+ set_piece_set(set);
+}
+
+ChessWidget::ChessWidget()
+ : ChessWidget("stelar7")
+{
+}
+
+ChessWidget::~ChessWidget()
+{
+}
+
+void ChessWidget::paint_event(GUI::PaintEvent& event)
+{
+ GUI::Widget::paint_event(event);
+
+ GUI::Painter painter(*this);
+ painter.add_clip_rect(event.rect());
+
+ size_t tile_width = width() / 8;
+ size_t tile_height = height() / 8;
+ unsigned coord_rank_file = (side() == Chess::Color::White) ? 0 : 7;
+
+ Chess::Board& active_board = (m_playback ? board_playback() : board());
+
+ Chess::Square::for_each([&](Chess::Square sq) {
+ Gfx::IntRect tile_rect;
+ if (side() == Chess::Color::White) {
+ tile_rect = { sq.file * tile_width, (7 - sq.rank) * tile_height, tile_width, tile_height };
+ } else {
+ tile_rect = { (7 - sq.file) * tile_width, sq.rank * tile_height, tile_width, tile_height };
+ }
+
+ painter.fill_rect(tile_rect, (sq.is_light()) ? board_theme().light_square_color : board_theme().dark_square_color);
+
+ if (active_board.last_move().has_value() && (active_board.last_move().value().to == sq || active_board.last_move().value().from == sq)) {
+ painter.fill_rect(tile_rect, m_move_highlight_color);
+ }
+
+ if (m_coordinates) {
+ auto coord = sq.to_algebraic();
+ auto text_color = (sq.is_light()) ? board_theme().dark_square_color : board_theme().light_square_color;
+
+ auto shrunken_rect = tile_rect;
+ shrunken_rect.shrink(4, 4);
+ if (sq.rank == coord_rank_file)
+ painter.draw_text(shrunken_rect, coord.substring_view(0, 1), Gfx::FontDatabase::default_bold_font(), Gfx::TextAlignment::BottomRight, text_color);
+
+ if (sq.file == coord_rank_file)
+ painter.draw_text(shrunken_rect, coord.substring_view(1, 1), Gfx::FontDatabase::default_bold_font(), Gfx::TextAlignment::TopLeft, text_color);
+ }
+
+ for (auto& m : m_board_markings) {
+ if (m.type() == BoardMarking::Type::Square && m.from == sq) {
+ Gfx::Color color = m.secondary_color ? m_marking_secondary_color : (m.alternate_color ? m_marking_alternate_color : m_marking_primary_color);
+ painter.fill_rect(tile_rect, color);
+ }
+ }
+
+ if (!(m_dragging_piece && sq == m_moving_square)) {
+ auto bmp = m_pieces.get(active_board.get_piece(sq));
+ if (bmp.has_value()) {
+ painter.draw_scaled_bitmap(tile_rect, *bmp.value(), bmp.value()->rect());
+ }
+ }
+
+ return IterationDecision::Continue;
+ });
+
+ auto draw_arrow = [&painter](Gfx::FloatPoint A, Gfx::FloatPoint B, float w1, float w2, float h, Gfx::Color color) {
+ float dx = B.x() - A.x();
+ float dy = A.y() - B.y();
+ float phi = atan2f(dy, dx);
+ float hdx = h * cos(phi);
+ float hdy = h * sin(phi);
+
+ Gfx::FloatPoint A1(A.x() - (w1 / 2) * cos(M_PI_2 - phi), A.y() - (w1 / 2) * sin(M_PI_2 - phi));
+ Gfx::FloatPoint B3(A.x() + (w1 / 2) * cos(M_PI_2 - phi), A.y() + (w1 / 2) * sin(M_PI_2 - phi));
+ Gfx::FloatPoint A2(A1.x() + (dx - hdx), A1.y() - (dy - hdy));
+ Gfx::FloatPoint B2(B3.x() + (dx - hdx), B3.y() - (dy - hdy));
+ Gfx::FloatPoint A3(A2.x() - w2 * cos(M_PI_2 - phi), A2.y() - w2 * sin(M_PI_2 - phi));
+ Gfx::FloatPoint B1(B2.x() + w2 * cos(M_PI_2 - phi), B2.y() + w2 * sin(M_PI_2 - phi));
+
+ auto path = Gfx::Path();
+ path.move_to(A);
+ path.line_to(A1);
+ path.line_to(A2);
+ path.line_to(A3);
+ path.line_to(B);
+ path.line_to(B1);
+ path.line_to(B2);
+ path.line_to(B3);
+ path.line_to(A);
+ path.close();
+
+ painter.fill_path(path, color, Gfx::Painter::WindingRule::EvenOdd);
+ };
+
+ for (auto& m : m_board_markings) {
+ if (m.type() == BoardMarking::Type::Arrow) {
+ Gfx::FloatPoint arrow_start;
+ Gfx::FloatPoint arrow_end;
+
+ if (side() == Chess::Color::White) {
+ arrow_start = { m.from.file * tile_width + tile_width / 2.0f, (7 - m.from.rank) * tile_height + tile_height / 2.0f };
+ arrow_end = { m.to.file * tile_width + tile_width / 2.0f, (7 - m.to.rank) * tile_height + tile_height / 2.0f };
+ } else {
+ arrow_start = { (7 - m.from.file) * tile_width + tile_width / 2.0f, m.from.rank * tile_height + tile_height / 2.0f };
+ arrow_end = { (7 - m.to.file) * tile_width + tile_width / 2.0f, m.to.rank * tile_height + tile_height / 2.0f };
+ }
+
+ Gfx::Color color = m.secondary_color ? m_marking_secondary_color : (m.alternate_color ? m_marking_primary_color : m_marking_alternate_color);
+ draw_arrow(arrow_start, arrow_end, tile_width / 8.0f, tile_width / 10.0f, tile_height / 2.5f, color);
+ }
+ }
+
+ if (m_dragging_piece) {
+ auto bmp = m_pieces.get(active_board.get_piece(m_moving_square));
+ if (bmp.has_value()) {
+ auto center = m_drag_point - Gfx::IntPoint(tile_width / 2, tile_height / 2);
+ painter.draw_scaled_bitmap({ center, { tile_width, tile_height } }, *bmp.value(), bmp.value()->rect());
+ }
+ }
+}
+
+void ChessWidget::mousedown_event(GUI::MouseEvent& event)
+{
+ GUI::Widget::mousedown_event(event);
+
+ if (event.button() == GUI::MouseButton::Right) {
+ m_current_marking.from = mouse_to_square(event);
+ return;
+ }
+ m_board_markings.clear();
+
+ auto square = mouse_to_square(event);
+ auto piece = board().get_piece(square);
+ if (drag_enabled() && piece.color == board().turn() && !m_playback) {
+ m_dragging_piece = true;
+ m_drag_point = event.position();
+ m_moving_square = square;
+ }
+
+ update();
+}
+
+void ChessWidget::mouseup_event(GUI::MouseEvent& event)
+{
+ GUI::Widget::mouseup_event(event);
+
+ if (event.button() == GUI::MouseButton::Right) {
+ m_current_marking.secondary_color = event.shift();
+ m_current_marking.alternate_color = event.ctrl();
+ m_current_marking.to = mouse_to_square(event);
+ auto match_index = m_board_markings.find_first_index(m_current_marking);
+ if (match_index.has_value()) {
+ m_board_markings.remove(match_index.value());
+ update();
+ return;
+ }
+ m_board_markings.append(m_current_marking);
+ update();
+ return;
+ }
+
+ if (!m_dragging_piece)
+ return;
+
+ m_dragging_piece = false;
+
+ auto target_square = mouse_to_square(event);
+
+ Chess::Move move = { m_moving_square, target_square };
+ if (board().is_promotion_move(move)) {
+ auto promotion_dialog = PromotionDialog::construct(*this);
+ if (promotion_dialog->exec() == PromotionDialog::ExecOK)
+ move.promote_to = promotion_dialog->selected_piece();
+ }
+
+ if (board().apply_move(move)) {
+ m_playback_move_number = board().moves().size();
+ m_playback = false;
+ m_board_playback = m_board;
+
+ if (board().game_result() != Chess::Board::Result::NotFinished) {
+ bool over = true;
+ String msg;
+ switch (board().game_result()) {
+ case Chess::Board::Result::CheckMate:
+ if (board().turn() == Chess::Color::White) {
+ msg = "Black wins by Checkmate.";
+ } else {
+ msg = "White wins by Checkmate.";
+ }
+ break;
+ case Chess::Board::Result::StaleMate:
+ msg = "Draw by Stalemate.";
+ break;
+ case Chess::Board::Result::FiftyMoveRule:
+ update();
+ if (GUI::MessageBox::show(window(), "50 moves have elapsed without a capture. Claim Draw?", "Claim Draw?",
+ GUI::MessageBox::Type::Information, GUI::MessageBox::InputType::YesNo)
+ == GUI::Dialog::ExecYes) {
+ msg = "Draw by 50 move rule.";
+ } else {
+ over = false;
+ }
+ break;
+ case Chess::Board::Result::SeventyFiveMoveRule:
+ msg = "Draw by 75 move rule.";
+ break;
+ case Chess::Board::Result::ThreeFoldRepetition:
+ update();
+ if (GUI::MessageBox::show(window(), "The same board state has repeated three times. Claim Draw?", "Claim Draw?",
+ GUI::MessageBox::Type::Information, GUI::MessageBox::InputType::YesNo)
+ == GUI::Dialog::ExecYes) {
+ msg = "Draw by threefold repetition.";
+ } else {
+ over = false;
+ }
+ break;
+ case Chess::Board::Result::FiveFoldRepetition:
+ msg = "Draw by fivefold repetition.";
+ break;
+ case Chess::Board::Result::InsufficientMaterial:
+ msg = "Draw by insufficient material.";
+ break;
+ default:
+ ASSERT_NOT_REACHED();
+ }
+ if (over) {
+ set_drag_enabled(false);
+ update();
+ GUI::MessageBox::show(window(), msg, "Game Over", GUI::MessageBox::Type::Information);
+ }
+ } else {
+ input_engine_move();
+ }
+ }
+
+ update();
+}
+
+void ChessWidget::mousemove_event(GUI::MouseEvent& event)
+{
+ GUI::Widget::mousemove_event(event);
+ if (!m_dragging_piece)
+ return;
+
+ m_drag_point = event.position();
+ update();
+}
+
+void ChessWidget::keydown_event(GUI::KeyEvent& event)
+{
+ switch (event.key()) {
+ case KeyCode::Key_Left:
+ playback_move(PlaybackDirection::Backward);
+ break;
+ case KeyCode::Key_Right:
+ playback_move(PlaybackDirection::Forward);
+ break;
+ case KeyCode::Key_Up:
+ playback_move(PlaybackDirection::Last);
+ break;
+ case KeyCode::Key_Down:
+ playback_move(PlaybackDirection::First);
+ break;
+ case KeyCode::Key_Home:
+ playback_move(PlaybackDirection::First);
+ break;
+ case KeyCode::Key_End:
+ playback_move(PlaybackDirection::Last);
+ break;
+ default:
+ return;
+ }
+ update();
+}
+
+static String set_path = String("/res/icons/chess/sets/");
+
+static RefPtr<Gfx::Bitmap> get_piece(const StringView& set, const StringView& image)
+{
+ StringBuilder builder;
+ builder.append(set_path);
+ builder.append(set);
+ builder.append('/');
+ builder.append(image);
+ return Gfx::Bitmap::load_from_file(builder.build());
+}
+
+void ChessWidget::set_piece_set(const StringView& set)
+{
+ m_piece_set = set;
+ m_pieces.set({ Chess::Color::White, Chess::Type::Pawn }, get_piece(set, "white-pawn.png"));
+ m_pieces.set({ Chess::Color::Black, Chess::Type::Pawn }, get_piece(set, "black-pawn.png"));
+ m_pieces.set({ Chess::Color::White, Chess::Type::Knight }, get_piece(set, "white-knight.png"));
+ m_pieces.set({ Chess::Color::Black, Chess::Type::Knight }, get_piece(set, "black-knight.png"));
+ m_pieces.set({ Chess::Color::White, Chess::Type::Bishop }, get_piece(set, "white-bishop.png"));
+ m_pieces.set({ Chess::Color::Black, Chess::Type::Bishop }, get_piece(set, "black-bishop.png"));
+ m_pieces.set({ Chess::Color::White, Chess::Type::Rook }, get_piece(set, "white-rook.png"));
+ m_pieces.set({ Chess::Color::Black, Chess::Type::Rook }, get_piece(set, "black-rook.png"));
+ m_pieces.set({ Chess::Color::White, Chess::Type::Queen }, get_piece(set, "white-queen.png"));
+ m_pieces.set({ Chess::Color::Black, Chess::Type::Queen }, get_piece(set, "black-queen.png"));
+ m_pieces.set({ Chess::Color::White, Chess::Type::King }, get_piece(set, "white-king.png"));
+ m_pieces.set({ Chess::Color::Black, Chess::Type::King }, get_piece(set, "black-king.png"));
+}
+
+Chess::Square ChessWidget::mouse_to_square(GUI::MouseEvent& event) const
+{
+ size_t tile_width = width() / 8;
+ size_t tile_height = height() / 8;
+
+ if (side() == Chess::Color::White) {
+ return { 7 - (event.y() / tile_height), event.x() / tile_width };
+ } else {
+ return { event.y() / tile_height, 7 - (event.x() / tile_width) };
+ }
+}
+
+RefPtr<Gfx::Bitmap> ChessWidget::get_piece_graphic(const Chess::Piece& piece) const
+{
+ return m_pieces.get(piece).value();
+}
+
+void ChessWidget::reset()
+{
+ m_board_markings.clear();
+ m_playback = false;
+ m_playback_move_number = 0;
+ m_board_playback = Chess::Board();
+ m_board = Chess::Board();
+ m_side = (arc4random() % 2) ? Chess::Color::White : Chess::Color::Black;
+ m_drag_enabled = true;
+ input_engine_move();
+ update();
+}
+
+void ChessWidget::set_board_theme(const StringView& name)
+{
+ // FIXME: Add some kind of themes.json
+ // The following Colors have been taken from lichess.org, but i'm pretty sure they took them from chess.com.
+ if (name == "Beige") {
+ m_board_theme = { "Beige", Color::from_rgb(0xb58863), Color::from_rgb(0xf0d9b5) };
+ } else if (name == "Green") {
+ m_board_theme = { "Green", Color::from_rgb(0x86a666), Color::from_rgb(0xffffdd) };
+ } else if (name == "Blue") {
+ m_board_theme = { "Blue", Color::from_rgb(0x8ca2ad), Color::from_rgb(0xdee3e6) };
+ } else {
+ set_board_theme("Beige");
+ }
+}
+
+bool ChessWidget::want_engine_move()
+{
+ if (!m_engine)
+ return false;
+ if (board().turn() == side())
+ return false;
+ return true;
+}
+
+void ChessWidget::input_engine_move()
+{
+ if (!want_engine_move())
+ return;
+
+ bool drag_was_enabled = drag_enabled();
+ if (drag_was_enabled)
+ set_drag_enabled(false);
+
+ set_override_cursor(Gfx::StandardCursor::Wait);
+ m_engine->get_best_move(board(), 4000, [this, drag_was_enabled](Chess::Move move) {
+ set_override_cursor(Gfx::StandardCursor::None);
+ if (!want_engine_move())
+ return;
+ set_drag_enabled(drag_was_enabled);
+ ASSERT(board().apply_move(move));
+ m_playback_move_number = m_board.moves().size();
+ m_playback = false;
+ m_board_markings.clear();
+ update();
+ });
+}
+
+void ChessWidget::playback_move(PlaybackDirection direction)
+{
+ if (m_board.moves().is_empty())
+ return;
+
+ m_playback = true;
+ m_board_markings.clear();
+
+ switch (direction) {
+ case PlaybackDirection::Backward:
+ if (m_playback_move_number == 0)
+ return;
+ m_board_playback = Chess::Board();
+ for (size_t i = 0; i < m_playback_move_number - 1; i++)
+ m_board_playback.apply_move(m_board.moves().at(i));
+ m_playback_move_number--;
+ break;
+ case PlaybackDirection::Forward:
+ if (m_playback_move_number + 1 > m_board.moves().size()) {
+ m_playback = false;
+ return;
+ }
+ m_board_playback.apply_move(m_board.moves().at(m_playback_move_number++));
+ if (m_playback_move_number == m_board.moves().size())
+ m_playback = false;
+ break;
+ case PlaybackDirection::First:
+ m_board_playback = Chess::Board();
+ m_playback_move_number = 0;
+ break;
+ case PlaybackDirection::Last:
+ while (m_playback) {
+ playback_move(PlaybackDirection::Forward);
+ }
+ break;
+ default:
+ ASSERT_NOT_REACHED();
+ }
+ update();
+}
+
+String ChessWidget::get_fen() const
+{
+ return m_playback ? m_board_playback.to_fen() : m_board.to_fen();
+}
+
+bool ChessWidget::import_pgn(const StringView& import_path)
+{
+ auto file_or_error = Core::File::open(import_path, Core::File::OpenMode::ReadOnly);
+ if (file_or_error.is_error()) {
+ warnln("Couldn't open '{}': {}", import_path, file_or_error.error());
+ return false;
+ }
+ auto& file = *file_or_error.value();
+
+ m_board = Chess::Board();
+
+ ByteBuffer bytes = file.read_all();
+ StringView content = bytes;
+ auto lines = content.lines();
+ StringView line;
+ size_t i = 0;
+
+ // Tag Pair Section
+ // FIXME: Parse these tags when they become relevant
+ do {
+ line = lines.at(i++);
+ } while (!line.is_empty() || i >= lines.size());
+
+ // Movetext Section
+ bool skip = false;
+ bool recursive_annotation = false;
+ bool future_expansion = false;
+ Chess::Color turn = Chess::Color::White;
+ String movetext;
+
+ for (size_t j = i; j < lines.size(); j++)
+ movetext = String::formatted("{}{}", movetext, lines.at(i).to_string());
+
+ for (auto token : movetext.split(' ')) {
+ token = token.trim_whitespace();
+
+ // FIXME: Parse all of these tokens when we start caring about them
+ if (token.ends_with("}")) {
+ skip = false;
+ continue;
+ }
+ if (skip)
+ continue;
+ if (token.starts_with("{")) {
+ if (token.ends_with("}"))
+ continue;
+ skip = true;
+ continue;
+ }
+ if (token.ends_with(")")) {
+ recursive_annotation = false;
+ continue;
+ }
+ if (recursive_annotation)
+ continue;
+ if (token.starts_with("(")) {
+ if (token.ends_with(")"))
+ continue;
+ recursive_annotation = true;
+ continue;
+ }
+ if (token.ends_with(">")) {
+ future_expansion = false;
+ continue;
+ }
+ if (future_expansion)
+ continue;
+ if (token.starts_with("<")) {
+ if (token.ends_with(">"))
+ continue;
+ future_expansion = true;
+ continue;
+ }
+ if (token.starts_with("$"))
+ continue;
+ if (token.contains("*"))
+ break;
+ // FIXME: When we become able to set more of the game state, fix these end results
+ if (token.contains("1-0")) {
+ m_board.set_resigned(Chess::Color::Black);
+ break;
+ }
+ if (token.contains("0-1")) {
+ m_board.set_resigned(Chess::Color::White);
+ break;
+ }
+ if (token.contains("1/2-1/2")) {
+ break;
+ }
+ if (!token.ends_with(".")) {
+ m_board.apply_move(Chess::Move::from_algebraic(token, turn, m_board));
+ turn = Chess::opposing_color(turn);
+ }
+ }
+
+ m_board_markings.clear();
+ m_board_playback = m_board;
+ m_playback_move_number = m_board_playback.moves().size();
+ m_playback = true;
+ update();
+
+ file.close();
+ return true;
+}
+
+bool ChessWidget::export_pgn(const StringView& export_path) const
+{
+ auto file_or_error = Core::File::open(export_path, Core::File::WriteOnly);
+ if (file_or_error.is_error()) {
+ warnln("Couldn't open '{}': {}", export_path, file_or_error.error());
+ return false;
+ }
+ auto& file = *file_or_error.value();
+
+ // Tag Pair Section
+ file.write("[Event \"Casual Game\"]\n");
+ file.write("[Site \"SerenityOS Chess\"]\n");
+ file.write(String::formatted("[Date \"{}\"]\n", Core::DateTime::now().to_string("%Y.%m.%d")));
+ file.write("[Round \"1\"]\n");
+
+ String username(getlogin());
+ const String player1 = (!username.is_empty() ? username : "?");
+ const String player2 = (!m_engine.is_null() ? "SerenityOS ChessEngine" : "?");
+ file.write(String::formatted("[White \"{}\"]\n", m_side == Chess::Color::White ? player1 : player2));
+ file.write(String::formatted("[Black \"{}\"]\n", m_side == Chess::Color::Black ? player1 : player2));
+
+ file.write(String::formatted("[Result \"{}\"]\n", Chess::Board::result_to_points(m_board.game_result(), m_board.turn())));
+ file.write("[WhiteElo \"?\"]\n");
+ file.write("[BlackElo \"?\"]\n");
+ file.write("[Variant \"Standard\"]\n");
+ file.write("[TimeControl \"-\"]\n");
+ file.write("[Annotator \"SerenityOS Chess\"]\n");
+ file.write("\n");
+
+ // Movetext Section
+ for (size_t i = 0, move_no = 1; i < m_board.moves().size(); i += 2, move_no++) {
+ const String white = m_board.moves().at(i).to_algebraic();
+
+ if (i + 1 < m_board.moves().size()) {
+ const String black = m_board.moves().at(i + 1).to_algebraic();
+ file.write(String::formatted("{}. {} {} ", move_no, white, black));
+ } else {
+ file.write(String::formatted("{}. {} ", move_no, white));
+ }
+ }
+
+ file.write("{ ");
+ file.write(Chess::Board::result_to_string(m_board.game_result(), m_board.turn()));
+ file.write(" } ");
+ file.write(Chess::Board::result_to_points(m_board.game_result(), m_board.turn()));
+ file.write("\n");
+
+ file.close();
+ return true;
+}
+
+void ChessWidget::flip_board()
+{
+ if (want_engine_move()) {
+ GUI::MessageBox::show(window(), "You can only flip the board on your turn.", "Flip Board", GUI::MessageBox::Type::Information);
+ return;
+ }
+ m_side = Chess::opposing_color(m_side);
+ input_engine_move();
+ update();
+}
+
+int ChessWidget::resign()
+{
+ if (want_engine_move()) {
+ GUI::MessageBox::show(window(), "You can only resign on your turn.", "Resign", GUI::MessageBox::Type::Information);
+ return -1;
+ }
+
+ auto result = GUI::MessageBox::show(window(), "Are you sure you wish to resign?", "Resign", GUI::MessageBox::Type::Warning, GUI::MessageBox::InputType::YesNo);
+ if (result != GUI::MessageBox::ExecYes)
+ return -1;
+
+ board().set_resigned(m_board.turn());
+
+ set_drag_enabled(false);
+ update();
+ const String msg = Chess::Board::result_to_string(m_board.game_result(), m_board.turn());
+ GUI::MessageBox::show(window(), msg, "Game Over", GUI::MessageBox::Type::Information);
+
+ return 0;
+}
diff --git a/Userland/Games/Chess/ChessWidget.h b/Userland/Games/Chess/ChessWidget.h
new file mode 100644
index 0000000000..772fe3f24a
--- /dev/null
+++ b/Userland/Games/Chess/ChessWidget.h
@@ -0,0 +1,147 @@
+/*
+ * Copyright (c) 2020, the SerenityOS developers.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include "Engine.h"
+#include <AK/HashMap.h>
+#include <AK/NonnullRefPtr.h>
+#include <AK/Optional.h>
+#include <AK/StringView.h>
+#include <LibChess/Chess.h>
+#include <LibGUI/Widget.h>
+#include <LibGfx/Bitmap.h>
+
+class ChessWidget final : public GUI::Widget {
+ C_OBJECT(ChessWidget)
+public:
+ ChessWidget();
+ ChessWidget(const StringView& set);
+ virtual ~ChessWidget() override;
+
+ virtual void paint_event(GUI::PaintEvent&) override;
+ virtual void mousedown_event(GUI::MouseEvent&) override;
+ virtual void mouseup_event(GUI::MouseEvent&) override;
+ virtual void mousemove_event(GUI::MouseEvent&) override;
+ virtual void keydown_event(GUI::KeyEvent&) override;
+
+ Chess::Board& board() { return m_board; };
+ const Chess::Board& board() const { return m_board; };
+
+ Chess::Board& board_playback() { return m_board_playback; };
+ const Chess::Board& board_playback() const { return m_board_playback; };
+
+ Chess::Color side() const { return m_side; };
+ void set_side(Chess::Color side) { m_side = side; };
+
+ void set_piece_set(const StringView& set);
+ const String& piece_set() const { return m_piece_set; };
+
+ Chess::Square mouse_to_square(GUI::MouseEvent& event) const;
+
+ bool drag_enabled() const { return m_drag_enabled; }
+ void set_drag_enabled(bool e) { m_drag_enabled = e; }
+ RefPtr<Gfx::Bitmap> get_piece_graphic(const Chess::Piece& piece) const;
+
+ String get_fen() const;
+ bool import_pgn(const StringView& import_path);
+ bool export_pgn(const StringView& export_path) const;
+
+ int resign();
+ void flip_board();
+ void reset();
+
+ struct BoardTheme {
+ String name;
+ Color dark_square_color;
+ Color light_square_color;
+ };
+
+ const BoardTheme& board_theme() const { return m_board_theme; }
+ void set_board_theme(const BoardTheme& theme) { m_board_theme = theme; }
+ void set_board_theme(const StringView& name);
+
+ enum class PlaybackDirection {
+ First,
+ Backward,
+ Forward,
+ Last
+ };
+
+ void playback_move(PlaybackDirection);
+
+ void set_engine(RefPtr<Engine> engine) { m_engine = engine; }
+
+ void input_engine_move();
+ bool want_engine_move();
+
+ void set_coordinates(bool coordinates) { m_coordinates = coordinates; }
+ bool coordinates() const { return m_coordinates; }
+
+ struct BoardMarking {
+ Chess::Square from { 50, 50 };
+ Chess::Square to { 50, 50 };
+ bool alternate_color { false };
+ bool secondary_color { false };
+ enum class Type {
+ Square,
+ Arrow,
+ None
+ };
+ Type type() const
+ {
+ if (from.in_bounds() && to.in_bounds() && from != to)
+ return Type::Arrow;
+ else if ((from.in_bounds() && !to.in_bounds()) || (from.in_bounds() && to.in_bounds() && from == to))
+ return Type::Square;
+
+ return Type::None;
+ }
+ bool operator==(const BoardMarking& other) const { return from == other.from && to == other.to; }
+ };
+
+private:
+ Chess::Board m_board;
+ Chess::Board m_board_playback;
+ bool m_playback { false };
+ size_t m_playback_move_number { 0 };
+ BoardMarking m_current_marking;
+ Vector<BoardMarking> m_board_markings;
+ BoardTheme m_board_theme { "Beige", Color::from_rgb(0xb58863), Color::from_rgb(0xf0d9b5) };
+ Color m_move_highlight_color { Color::from_rgba(0x66ccee00) };
+ Color m_marking_primary_color { Color::from_rgba(0x66ff0000) };
+ Color m_marking_alternate_color { Color::from_rgba(0x66ffaa00) };
+ Color m_marking_secondary_color { Color::from_rgba(0x6655dd55) };
+ Chess::Color m_side { Chess::Color::White };
+ HashMap<Chess::Piece, RefPtr<Gfx::Bitmap>> m_pieces;
+ String m_piece_set;
+ Chess::Square m_moving_square { 50, 50 };
+ Gfx::IntPoint m_drag_point;
+ bool m_dragging_piece { false };
+ bool m_drag_enabled { true };
+ RefPtr<Engine> m_engine;
+ bool m_coordinates { true };
+};
diff --git a/Userland/Games/Chess/Engine.cpp b/Userland/Games/Chess/Engine.cpp
new file mode 100644
index 0000000000..f02a4a5f5b
--- /dev/null
+++ b/Userland/Games/Chess/Engine.cpp
@@ -0,0 +1,88 @@
+/*
+ * Copyright (c) 2020, the SerenityOS developers.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "Engine.h"
+#include <LibCore/File.h>
+#include <fcntl.h>
+#include <spawn.h>
+#include <stdio.h>
+#include <stdlib.h>
+
+Engine::~Engine()
+{
+ if (m_pid != -1)
+ kill(m_pid, SIGINT);
+}
+
+Engine::Engine(const StringView& command)
+{
+ int wpipefds[2];
+ int rpipefds[2];
+ if (pipe2(wpipefds, O_CLOEXEC) < 0) {
+ perror("pipe2");
+ ASSERT_NOT_REACHED();
+ }
+
+ if (pipe2(rpipefds, O_CLOEXEC) < 0) {
+ perror("pipe2");
+ ASSERT_NOT_REACHED();
+ }
+
+ posix_spawn_file_actions_t file_actions;
+ posix_spawn_file_actions_init(&file_actions);
+ posix_spawn_file_actions_adddup2(&file_actions, wpipefds[0], STDIN_FILENO);
+ posix_spawn_file_actions_adddup2(&file_actions, rpipefds[1], STDOUT_FILENO);
+
+ String cstr(command);
+ const char* argv[] = { cstr.characters(), nullptr };
+ if (posix_spawnp(&m_pid, cstr.characters(), &file_actions, nullptr, const_cast<char**>(argv), environ) < 0) {
+ perror("posix_spawnp");
+ ASSERT_NOT_REACHED();
+ }
+
+ posix_spawn_file_actions_destroy(&file_actions);
+
+ close(wpipefds[0]);
+ close(rpipefds[1]);
+
+ auto infile = Core::File::construct();
+ infile->open(rpipefds[0], Core::IODevice::ReadOnly, Core::File::ShouldCloseFileDescriptor::Yes);
+ set_in(infile);
+
+ auto outfile = Core::File::construct();
+ outfile->open(wpipefds[1], Core::IODevice::WriteOnly, Core::File::ShouldCloseFileDescriptor::Yes);
+ set_out(outfile);
+
+ send_command(Chess::UCI::UCICommand());
+}
+
+void Engine::handle_bestmove(const Chess::UCI::BestMoveCommand& command)
+{
+ if (m_bestmove_callback)
+ m_bestmove_callback(command.move());
+
+ m_bestmove_callback = nullptr;
+}
diff --git a/Userland/Games/Chess/Engine.h b/Userland/Games/Chess/Engine.h
new file mode 100644
index 0000000000..d07ef9cfcf
--- /dev/null
+++ b/Userland/Games/Chess/Engine.h
@@ -0,0 +1,58 @@
+/*
+ * Copyright (c) 2020, the SerenityOS developers.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include <AK/Function.h>
+#include <LibChess/UCIEndpoint.h>
+#include <sys/types.h>
+
+class Engine : public Chess::UCI::Endpoint {
+ C_OBJECT(Engine)
+public:
+ virtual ~Engine() override;
+
+ Engine(const StringView& command);
+
+ Engine(const Engine&) = delete;
+ Engine& operator=(const Engine&) = delete;
+
+ virtual void handle_bestmove(const Chess::UCI::BestMoveCommand&);
+
+ template<typename Callback>
+ void get_best_move(const Chess::Board& board, int time_limit, Callback&& callback)
+ {
+ send_command(Chess::UCI::PositionCommand({}, board.moves()));
+ Chess::UCI::GoCommand go_command;
+ go_command.movetime = time_limit;
+ send_command(go_command);
+ m_bestmove_callback = move(callback);
+ }
+
+private:
+ Function<void(Chess::Move)> m_bestmove_callback;
+ pid_t m_pid { -1 };
+};
diff --git a/Userland/Games/Chess/PromotionDialog.cpp b/Userland/Games/Chess/PromotionDialog.cpp
new file mode 100644
index 0000000000..17e1e7a004
--- /dev/null
+++ b/Userland/Games/Chess/PromotionDialog.cpp
@@ -0,0 +1,59 @@
+/*
+ * Copyright (c) 2020, the SerenityOS developers.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "PromotionDialog.h"
+#include <LibGUI/BoxLayout.h>
+#include <LibGUI/Button.h>
+#include <LibGUI/Frame.h>
+
+PromotionDialog::PromotionDialog(ChessWidget& chess_widget)
+ : Dialog(chess_widget.window())
+ , m_selected_piece(Chess::Type::None)
+{
+ set_title("Choose piece to promote to");
+ set_icon(chess_widget.window()->icon());
+ resize(70 * 4, 70);
+
+ auto& main_widget = set_main_widget<GUI::Frame>();
+ main_widget.set_frame_shape(Gfx::FrameShape::Container);
+ main_widget.set_fill_with_background_color(true);
+ main_widget.set_layout<GUI::HorizontalBoxLayout>();
+
+ for (auto& type : Vector({ Chess::Type::Queen, Chess::Type::Knight, Chess::Type::Rook, Chess::Type::Bishop })) {
+ auto& button = main_widget.add<GUI::Button>("");
+ button.set_fixed_height(70);
+ button.set_icon(chess_widget.get_piece_graphic({ chess_widget.board().turn(), type }));
+ button.on_click = [this, type](auto) {
+ m_selected_piece = type;
+ done(ExecOK);
+ };
+ }
+}
+
+void PromotionDialog::event(Core::Event& event)
+{
+ Dialog::event(event);
+}
diff --git a/Userland/Games/Chess/PromotionDialog.h b/Userland/Games/Chess/PromotionDialog.h
new file mode 100644
index 0000000000..78c0f3ad52
--- /dev/null
+++ b/Userland/Games/Chess/PromotionDialog.h
@@ -0,0 +1,42 @@
+/*
+ * Copyright (c) 2020, the SerenityOS developers.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include "ChessWidget.h"
+#include <LibGUI/Dialog.h>
+
+class PromotionDialog final : public GUI::Dialog {
+ C_OBJECT(PromotionDialog)
+public:
+ Chess::Type selected_piece() const { return m_selected_piece; }
+
+private:
+ explicit PromotionDialog(ChessWidget& chess_widget);
+ virtual void event(Core::Event&) override;
+
+ Chess::Type m_selected_piece;
+};
diff --git a/Userland/Games/Chess/main.cpp b/Userland/Games/Chess/main.cpp
new file mode 100644
index 0000000000..213d5a8a4f
--- /dev/null
+++ b/Userland/Games/Chess/main.cpp
@@ -0,0 +1,231 @@
+/*
+ * Copyright (c) 2020, the SerenityOS developers.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "ChessWidget.h"
+#include <LibCore/ConfigFile.h>
+#include <LibCore/DirIterator.h>
+#include <LibGUI/ActionGroup.h>
+#include <LibGUI/Application.h>
+#include <LibGUI/Clipboard.h>
+#include <LibGUI/FilePicker.h>
+#include <LibGUI/Icon.h>
+#include <LibGUI/Menu.h>
+#include <LibGUI/MenuBar.h>
+#include <LibGUI/MessageBox.h>
+#include <LibGUI/Window.h>
+
+int main(int argc, char** argv)
+{
+ auto app = GUI::Application::construct(argc, argv);
+ auto app_icon = GUI::Icon::default_icon("app-chess");
+
+ auto window = GUI::Window::construct();
+ auto& widget = window->set_main_widget<ChessWidget>();
+
+ RefPtr<Core::ConfigFile> config = Core::ConfigFile::get_for_app("Chess");
+
+ if (pledge("stdio rpath accept wpath cpath shared_buffer proc exec", nullptr) < 0) {
+ perror("pledge");
+ return 1;
+ }
+
+ if (unveil("/res", "r") < 0) {
+ perror("unveil");
+ return 1;
+ }
+
+ if (unveil(config->file_name().characters(), "crw") < 0) {
+ perror("unveil");
+ return 1;
+ }
+
+ if (unveil("/bin/ChessEngine", "x") < 0) {
+ perror("unveil");
+ return 1;
+ }
+
+ if (unveil("/etc/passwd", "r") < 0) {
+ perror("unveil");
+ return 1;
+ }
+
+ if (unveil(Core::StandardPaths::home_directory().characters(), "wcbr") < 0) {
+ perror("unveil");
+ return 1;
+ }
+
+ if (unveil(nullptr, nullptr) < 0) {
+ perror("unveil");
+ return 1;
+ }
+
+ auto size = config->read_num_entry("Display", "size", 512);
+ window->set_title("Chess");
+ window->resize(size, size);
+ window->set_size_increment({ 8, 8 });
+ window->set_resize_aspect_ratio(1, 1);
+
+ window->set_icon(app_icon.bitmap_for_size(16));
+
+ widget.set_piece_set(config->read_entry("Style", "PieceSet", "stelar7"));
+ widget.set_board_theme(config->read_entry("Style", "BoardTheme", "Beige"));
+ widget.set_coordinates(config->read_bool_entry("Style", "Coordinates", true));
+
+ auto menubar = GUI::MenuBar::construct();
+ auto& app_menu = menubar->add_menu("Chess");
+
+ app_menu.add_action(GUI::Action::create("Resign", { Mod_None, Key_F3 }, [&](auto&) {
+ widget.resign();
+ }));
+ app_menu.add_action(GUI::Action::create("Flip Board", { Mod_Ctrl, Key_F }, [&](auto&) {
+ widget.flip_board();
+ }));
+ app_menu.add_separator();
+
+ app_menu.add_action(GUI::Action::create("Import PGN...", { Mod_Ctrl, Key_O }, [&](auto&) {
+ Optional<String> import_path = GUI::FilePicker::get_open_filepath(window);
+
+ if (!import_path.has_value())
+ return;
+
+ if (!widget.import_pgn(import_path.value())) {
+ GUI::MessageBox::show(window, "Unable to import game.\n", "Error", GUI::MessageBox::Type::Error);
+ return;
+ }
+
+ dbgln("Imported PGN file from {}", import_path.value());
+ }));
+ app_menu.add_action(GUI::Action::create("Export PGN...", { Mod_Ctrl, Key_S }, [&](auto&) {
+ Optional<String> export_path = GUI::FilePicker::get_save_filepath(window, "Untitled", "pgn");
+
+ if (!export_path.has_value())
+ return;
+
+ if (!widget.export_pgn(export_path.value())) {
+ GUI::MessageBox::show(window, "Unable to export game.\n", "Error", GUI::MessageBox::Type::Error);
+ return;
+ }
+
+ dbgln("Exported PGN file to {}", export_path.value());
+ }));
+ app_menu.add_action(GUI::Action::create("Copy FEN", { Mod_Ctrl, Key_C }, [&](auto&) {
+ GUI::Clipboard::the().set_data(widget.get_fen().bytes());
+ GUI::MessageBox::show(window, "Board state copied to clipboard as FEN.", "Copy FEN", GUI::MessageBox::Type::Information);
+ }));
+ app_menu.add_separator();
+
+ app_menu.add_action(GUI::Action::create("New game", { Mod_None, Key_F2 }, [&](auto&) {
+ if (widget.board().game_result() == Chess::Board::Result::NotFinished) {
+ if (widget.resign() < 0)
+ return;
+ }
+ widget.reset();
+ }));
+ app_menu.add_separator();
+ app_menu.add_action(GUI::CommonActions::make_quit_action([](auto&) {
+ GUI::Application::the()->quit();
+ }));
+
+ auto& style_menu = menubar->add_menu("Style");
+ GUI::ActionGroup piece_set_action_group;
+ piece_set_action_group.set_exclusive(true);
+ auto& piece_set_menu = style_menu.add_submenu("Piece Set");
+ piece_set_menu.set_icon(app_icon.bitmap_for_size(16));
+
+ Core::DirIterator di("/res/icons/chess/sets/", Core::DirIterator::SkipParentAndBaseDir);
+ while (di.has_next()) {
+ auto set = di.next_path();
+ auto action = GUI::Action::create_checkable(set, [&](auto& action) {
+ widget.set_piece_set(action.text());
+ widget.update();
+ config->write_entry("Style", "PieceSet", action.text());
+ config->sync();
+ });
+
+ piece_set_action_group.add_action(*action);
+ if (widget.piece_set() == set)
+ action->set_checked(true);
+ piece_set_menu.add_action(*action);
+ }
+
+ GUI::ActionGroup board_theme_action_group;
+ board_theme_action_group.set_exclusive(true);
+ auto& board_theme_menu = style_menu.add_submenu("Board Theme");
+ board_theme_menu.set_icon(Gfx::Bitmap::load_from_file("/res/icons/chess/mini-board.png"));
+
+ for (auto& theme : Vector({ "Beige", "Green", "Blue" })) {
+ auto action = GUI::Action::create_checkable(theme, [&](auto& action) {
+ widget.set_board_theme(action.text());
+ widget.update();
+ config->write_entry("Style", "BoardTheme", action.text());
+ config->sync();
+ });
+ board_theme_action_group.add_action(*action);
+ if (widget.board_theme().name == theme)
+ action->set_checked(true);
+ board_theme_menu.add_action(*action);
+ }
+
+ auto coordinates_action = GUI::Action::create_checkable("Coordinates", [&](auto& action) {
+ widget.set_coordinates(action.is_checked());
+ widget.update();
+ config->write_bool_entry("Style", "Coordinates", action.is_checked());
+ config->sync();
+ });
+ coordinates_action->set_checked(widget.coordinates());
+ style_menu.add_action(coordinates_action);
+
+ auto& engine_menu = menubar->add_menu("Engine");
+
+ GUI::ActionGroup engines_action_group;
+ engines_action_group.set_exclusive(true);
+ auto& engine_submenu = engine_menu.add_submenu("Engine");
+ for (auto& engine : Vector({ "Human", "ChessEngine" })) {
+ auto action = GUI::Action::create_checkable(engine, [&](auto& action) {
+ if (action.text() == "Human") {
+ widget.set_engine(nullptr);
+ } else {
+ widget.set_engine(Engine::construct(action.text()));
+ widget.input_engine_move();
+ }
+ });
+ engines_action_group.add_action(*action);
+ if (engine == String("Human"))
+ action->set_checked(true);
+
+ engine_submenu.add_action(*action);
+ }
+
+ auto& help_menu = menubar->add_menu("Help");
+ help_menu.add_action(GUI::CommonActions::make_about_action("Chess", app_icon, window));
+
+ app->set_menubar(move(menubar));
+
+ window->show();
+ widget.reset();
+
+ return app->exec();
+}
diff --git a/Userland/Games/Conway/CMakeLists.txt b/Userland/Games/Conway/CMakeLists.txt
new file mode 100644
index 0000000000..18312c363a
--- /dev/null
+++ b/Userland/Games/Conway/CMakeLists.txt
@@ -0,0 +1,7 @@
+set(SOURCES
+ main.cpp
+ Game.cpp
+)
+
+serenity_app(Conway ICON app-conway)
+target_link_libraries(Conway LibGUI)
diff --git a/Userland/Games/Conway/Game.cpp b/Userland/Games/Conway/Game.cpp
new file mode 100644
index 0000000000..e35603bcbb
--- /dev/null
+++ b/Userland/Games/Conway/Game.cpp
@@ -0,0 +1,120 @@
+/*
+ * Copyright (c) 2021, the SerenityOS developers.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "Game.h"
+#include <LibGUI/Painter.h>
+#include <stdlib.h>
+#include <time.h>
+
+Game::Game()
+{
+ srand(time(nullptr));
+ reset();
+}
+
+Game::~Game()
+{
+}
+
+void Game::reset()
+{
+ stop_timer();
+ seed_universe();
+ start_timer(m_sleep);
+ update();
+}
+
+void Game::seed_universe()
+{
+ for (int y = 0; y < m_rows; y++) {
+ for (int x = 0; x < m_columns; x++) {
+ m_universe[y][x] = (arc4random() % 2) ? 1 : 0;
+ }
+ }
+}
+
+void Game::update_universe()
+{
+ bool new_universe[m_rows][m_columns];
+
+ for (int y = 0; y < m_rows; y++) {
+ for (int x = 0; x < m_columns; x++) {
+ int n = 0;
+ auto cell = m_universe[y][x];
+
+ for (int y1 = y - 1; y1 <= y + 1; y1++) {
+ for (int x1 = x - 1; x1 <= x + 1; x1++) {
+ if (m_universe[(y1 + m_rows) % m_rows][(x1 + m_columns) % m_columns]) {
+ n++;
+ }
+ }
+ }
+
+ if (cell)
+ n--;
+
+ if (n == 3 || (n == 2 && cell))
+ new_universe[y][x] = true;
+ else
+ new_universe[y][x] = false;
+ }
+ }
+
+ for (int y = 0; y < m_rows; y++) {
+ for (int x = 0; x < m_columns; x++) {
+ m_universe[y][x] = new_universe[y][x];
+ }
+ }
+}
+
+void Game::timer_event(Core::TimerEvent&)
+{
+ update_universe();
+ update();
+}
+
+void Game::paint_event(GUI::PaintEvent& event)
+{
+ GUI::Painter painter(*this);
+ painter.add_clip_rect(event.rect());
+ painter.fill_rect(event.rect(), m_dead_color);
+ auto game_rect = rect();
+ auto cell_size = Gfx::IntSize(game_rect.width() / m_columns, game_rect.height() / m_rows);
+ auto x_margin = (game_rect.width() - (cell_size.width() * m_columns)) / 2;
+ auto y_margin = (game_rect.height() - (cell_size.height() * m_rows)) / 2;
+
+ for (int y = 0; y < m_rows; y++) {
+ for (int x = 0; x < m_columns; x++) {
+ Gfx::IntRect rect {
+ x * cell_size.width() + x_margin,
+ y * cell_size.height() + y_margin,
+ cell_size.width(),
+ cell_size.height()
+ };
+ painter.fill_rect(rect, m_universe[y][x] ? m_alive_color : m_dead_color);
+ }
+ }
+}
diff --git a/Userland/Games/Conway/Game.h b/Userland/Games/Conway/Game.h
new file mode 100644
index 0000000000..6733409548
--- /dev/null
+++ b/Userland/Games/Conway/Game.h
@@ -0,0 +1,52 @@
+/*
+ * Copyright (c) 2021, the SerenityOS developers.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include <LibGUI/Widget.h>
+
+class Game : public GUI::Widget {
+ C_OBJECT(Game)
+public:
+ virtual ~Game() override;
+ void reset();
+
+private:
+ Game();
+ virtual void paint_event(GUI::PaintEvent&) override;
+ virtual void timer_event(Core::TimerEvent&) override;
+
+ void seed_universe();
+ void update_universe();
+
+ Gfx::Color m_alive_color { Color::Green };
+ Gfx::Color m_dead_color { Color::Black };
+ int m_rows { 200 };
+ int m_columns { 200 };
+ int m_sleep { 100 };
+
+ bool m_universe[200][200];
+};
diff --git a/Userland/Games/Conway/main.cpp b/Userland/Games/Conway/main.cpp
new file mode 100644
index 0000000000..ae57220f45
--- /dev/null
+++ b/Userland/Games/Conway/main.cpp
@@ -0,0 +1,90 @@
+/*
+ * Copyright (c) 2021, the SerenityOS developers.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "Game.h"
+#include <LibGUI/Application.h>
+#include <LibGUI/Icon.h>
+#include <LibGUI/Menu.h>
+#include <LibGUI/MenuBar.h>
+#include <LibGUI/Window.h>
+#include <stdio.h>
+
+int main(int argc, char** argv)
+{
+ if (pledge("stdio rpath wpath cpath shared_buffer accept cpath unix fattr", nullptr) < 0) {
+ perror("pledge");
+ return 1;
+ }
+
+ auto app = GUI::Application::construct(argc, argv);
+
+ if (pledge("stdio rpath shared_buffer accept", nullptr) < 0) {
+ perror("pledge");
+ return 1;
+ }
+
+ if (unveil("/res", "r") < 0) {
+ perror("unveil");
+ return 1;
+ }
+
+ if (unveil(nullptr, nullptr) < 0) {
+ perror("unveil");
+ return 1;
+ }
+
+ auto app_icon = GUI::Icon::default_icon("app-conway");
+
+ auto window = GUI::Window::construct();
+
+ window->set_title("Conway");
+ window->resize(400, 400);
+ window->set_double_buffering_enabled(true);
+ window->set_icon(app_icon.bitmap_for_size(16));
+
+ auto& game = window->set_main_widget<Game>();
+
+ auto menubar = GUI::MenuBar::construct();
+
+ auto& app_menu = menubar->add_menu("Conway");
+
+ app_menu.add_action(GUI::Action::create("Reset", { Mod_None, Key_F2 }, [&](auto&) {
+ game.reset();
+ }));
+ app_menu.add_separator();
+ app_menu.add_action(GUI::CommonActions::make_quit_action([](auto&) {
+ GUI::Application::the()->quit();
+ }));
+
+ auto& help_menu = menubar->add_menu("Help");
+ help_menu.add_action(GUI::CommonActions::make_about_action("Conway", app_icon, window));
+
+ app->set_menubar(move(menubar));
+
+ window->show();
+
+ return app->exec();
+}
diff --git a/Userland/Games/Minesweeper/CMakeLists.txt b/Userland/Games/Minesweeper/CMakeLists.txt
new file mode 100644
index 0000000000..466227edb6
--- /dev/null
+++ b/Userland/Games/Minesweeper/CMakeLists.txt
@@ -0,0 +1,7 @@
+set(SOURCES
+ Field.cpp
+ main.cpp
+)
+
+serenity_app(Minesweeper ICON app-minesweeper)
+target_link_libraries(Minesweeper LibGUI)
diff --git a/Userland/Games/Minesweeper/Field.cpp b/Userland/Games/Minesweeper/Field.cpp
new file mode 100644
index 0000000000..351a900094
--- /dev/null
+++ b/Userland/Games/Minesweeper/Field.cpp
@@ -0,0 +1,541 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "Field.h"
+#include <AK/HashTable.h>
+#include <AK/Queue.h>
+#include <LibCore/ConfigFile.h>
+#include <LibGUI/Button.h>
+#include <LibGUI/Label.h>
+#include <LibGUI/Painter.h>
+#include <LibGfx/Palette.h>
+#include <time.h>
+#include <unistd.h>
+
+class SquareButton final : public GUI::Button {
+ C_OBJECT(SquareButton);
+
+public:
+ Function<void()> on_right_click;
+ Function<void()> on_middle_click;
+
+ virtual void mousedown_event(GUI::MouseEvent& event) override
+ {
+ if (event.button() == GUI::MouseButton::Right) {
+ if (on_right_click)
+ on_right_click();
+ }
+ if (event.button() == GUI::MouseButton::Middle) {
+ if (on_middle_click)
+ on_middle_click();
+ }
+ GUI::Button::mousedown_event(event);
+ }
+
+private:
+ SquareButton() { }
+};
+
+class SquareLabel final : public GUI::Label {
+ C_OBJECT(SquareLabel);
+
+public:
+ Function<void()> on_chord_click;
+
+ virtual void mousedown_event(GUI::MouseEvent& event) override
+ {
+ if (event.button() == GUI::MouseButton::Right || event.button() == GUI::MouseButton::Left) {
+ if (event.buttons() == (GUI::MouseButton::Right | GUI::MouseButton::Left) || m_square.field->is_single_chording()) {
+ m_chord = true;
+ m_square.field->set_chord_preview(m_square, true);
+ }
+ }
+ if (event.button() == GUI::MouseButton::Middle) {
+ m_square.field->for_each_square([](auto& square) {
+ if (square.is_considering) {
+ square.is_considering = false;
+ square.button->set_icon(nullptr);
+ }
+ });
+ }
+ GUI::Label::mousedown_event(event);
+ }
+
+ virtual void mousemove_event(GUI::MouseEvent& event) override
+ {
+ if (m_chord) {
+ if (rect().contains(event.position())) {
+ m_square.field->set_chord_preview(m_square, true);
+ } else {
+ m_square.field->set_chord_preview(m_square, false);
+ }
+ }
+ GUI::Label::mousemove_event(event);
+ }
+
+ virtual void mouseup_event(GUI::MouseEvent& event) override
+ {
+ if (m_chord) {
+ if (event.button() == GUI::MouseButton::Left || event.button() == GUI::MouseButton::Right) {
+ if (rect().contains(event.position())) {
+ if (on_chord_click)
+ on_chord_click();
+ }
+ m_chord = false;
+ }
+ }
+ m_square.field->set_chord_preview(m_square, m_chord);
+ GUI::Label::mouseup_event(event);
+ }
+
+private:
+ explicit SquareLabel(Square& square)
+ : m_square(square)
+ {
+ }
+
+ Square& m_square;
+ bool m_chord { false };
+};
+
+Field::Field(GUI::Label& flag_label, GUI::Label& time_label, GUI::Button& face_button, Function<void(Gfx::IntSize)> on_size_changed)
+ : m_face_button(face_button)
+ , m_flag_label(flag_label)
+ , m_time_label(time_label)
+ , m_on_size_changed(move(on_size_changed))
+{
+ srand(time(nullptr));
+ m_timer = add<Core::Timer>();
+ m_timer->on_timeout = [this] {
+ ++m_time_elapsed;
+ m_time_label.set_text(String::formatted("{}.{}", m_time_elapsed / 10, m_time_elapsed % 10));
+ };
+ m_timer->set_interval(100);
+ m_mine_bitmap = Gfx::Bitmap::load_from_file("/res/icons/minesweeper/mine.png");
+ m_flag_bitmap = Gfx::Bitmap::load_from_file("/res/icons/minesweeper/flag.png");
+ m_badflag_bitmap = Gfx::Bitmap::load_from_file("/res/icons/minesweeper/badflag.png");
+ m_consider_bitmap = Gfx::Bitmap::load_from_file("/res/icons/minesweeper/consider.png");
+ m_default_face_bitmap = Gfx::Bitmap::load_from_file("/res/icons/minesweeper/face-default.png");
+ m_good_face_bitmap = Gfx::Bitmap::load_from_file("/res/icons/minesweeper/face-good.png");
+ m_bad_face_bitmap = Gfx::Bitmap::load_from_file("/res/icons/minesweeper/face-bad.png");
+ for (int i = 0; i < 8; ++i)
+ m_number_bitmap[i] = Gfx::Bitmap::load_from_file(String::format("/res/icons/minesweeper/%u.png", i + 1));
+
+ set_fill_with_background_color(true);
+ reset();
+
+ m_face_button.on_click = [this](auto) { reset(); };
+ set_face(Face::Default);
+
+ {
+ auto config = Core::ConfigFile::get_for_app("Minesweeper");
+ bool single_chording = config->read_num_entry("Minesweeper", "SingleChording", false);
+ int mine_count = config->read_num_entry("Game", "MineCount", 10);
+ int rows = config->read_num_entry("Game", "Rows", 9);
+ int columns = config->read_num_entry("Game", "Columns", 9);
+
+ // Do a quick sanity check to make sure the user hasn't tried anything crazy
+ if (mine_count > rows * columns || rows <= 0 || columns <= 0 || mine_count <= 0)
+ set_field_size(9, 9, 10);
+ else
+ set_field_size(rows, columns, mine_count);
+
+ set_single_chording(single_chording);
+ }
+}
+
+Field::~Field()
+{
+}
+
+void Field::set_face(Face face)
+{
+ switch (face) {
+ case Face::Default:
+ m_face_button.set_icon(*m_default_face_bitmap);
+ break;
+ case Face::Good:
+ m_face_button.set_icon(*m_good_face_bitmap);
+ break;
+ case Face::Bad:
+ m_face_button.set_icon(*m_bad_face_bitmap);
+ break;
+ }
+}
+
+template<typename Callback>
+void Square::for_each_neighbor(Callback callback)
+{
+ size_t r = row;
+ size_t c = column;
+ if (r > 0) // Up
+ callback(field->square(r - 1, c));
+ if (c > 0) // Left
+ callback(field->square(r, c - 1));
+ if (r < (field->m_rows - 1)) // Down
+ callback(field->square(r + 1, c));
+ if (c < (field->m_columns - 1)) // Right
+ callback(field->square(r, c + 1));
+ if (r > 0 && c > 0) // UpLeft
+ callback(field->square(r - 1, c - 1));
+ if (r > 0 && c < (field->m_columns - 1)) // UpRight
+ callback(field->square(r - 1, c + 1));
+ if (r < (field->m_rows - 1) && c > 0) // DownLeft
+ callback(field->square(r + 1, c - 1));
+ if (r < (field->m_rows - 1) && c < (field->m_columns - 1)) // DownRight
+ callback(field->square(r + 1, c + 1));
+}
+
+void Field::reset()
+{
+ m_first_click = true;
+ set_updates_enabled(false);
+ m_time_elapsed = 0;
+ m_time_label.set_text("0");
+ m_flags_left = m_mine_count;
+ m_flag_label.set_text(String::number(m_flags_left));
+ m_timer->stop();
+ set_greedy_for_hits(false);
+ set_face(Face::Default);
+
+ m_squares.resize(max(m_squares.size(), rows() * columns()));
+
+ for (int i = rows() * columns(); i < static_cast<int>(m_squares.size()); ++i) {
+ auto& square = m_squares[i];
+ square->button->set_visible(false);
+ square->label->set_visible(false);
+ }
+
+ HashTable<int> mines;
+ while (mines.size() != m_mine_count) {
+ int location = rand() % (rows() * columns());
+ if (!mines.contains(location))
+ mines.set(location);
+ }
+
+ size_t i = 0;
+ for (size_t r = 0; r < rows(); ++r) {
+ for (size_t c = 0; c < columns(); ++c) {
+ if (!m_squares[i])
+ m_squares[i] = make<Square>();
+ Gfx::IntRect rect = { frame_thickness() + static_cast<int>(c) * square_size(), frame_thickness() + static_cast<int>(r) * square_size(), square_size(), square_size() };
+ auto& square = this->square(r, c);
+ square.field = this;
+ square.row = r;
+ square.column = c;
+ square.has_mine = mines.contains(i);
+ square.has_flag = false;
+ square.is_considering = false;
+ square.is_swept = false;
+ if (!square.label) {
+ square.label = add<SquareLabel>(square);
+ // Square with mine will be filled with background color later, i.e. red
+ auto palette = square.label->palette();
+ palette.set_color(Gfx::ColorRole::Base, Color::from_rgb(0xff4040));
+ square.label->set_palette(palette);
+ square.label->set_background_role(Gfx::ColorRole::Base);
+ }
+ square.label->set_fill_with_background_color(false);
+ square.label->set_relative_rect(rect);
+ square.label->set_visible(false);
+ square.label->set_icon(square.has_mine ? m_mine_bitmap : nullptr);
+ if (!square.button) {
+ square.button = add<SquareButton>();
+ square.button->on_click = [this, &square](auto) {
+ on_square_clicked(square);
+ };
+ square.button->on_right_click = [this, &square] {
+ on_square_right_clicked(square);
+ };
+ square.button->on_middle_click = [this, &square] {
+ on_square_middle_clicked(square);
+ };
+ square.label->on_chord_click = [this, &square] {
+ on_square_chorded(square);
+ };
+ }
+ square.button->set_checked(false);
+ square.button->set_icon(nullptr);
+ square.button->set_relative_rect(rect);
+ square.button->set_visible(true);
+
+ ++i;
+ }
+ }
+
+ for (size_t r = 0; r < rows(); ++r) {
+ for (size_t c = 0; c < columns(); ++c) {
+ auto& square = this->square(r, c);
+ size_t number = 0;
+ square.for_each_neighbor([&number](auto& neighbor) {
+ number += neighbor.has_mine;
+ });
+ square.number = number;
+ if (square.has_mine)
+ continue;
+ if (square.number)
+ square.label->set_icon(m_number_bitmap[square.number - 1]);
+ }
+ }
+
+ m_unswept_empties = rows() * columns() - m_mine_count;
+ set_updates_enabled(true);
+}
+
+void Field::flood_fill(Square& square)
+{
+ Queue<Square*> queue;
+ queue.enqueue(&square);
+
+ while (!queue.is_empty()) {
+ Square* s = queue.dequeue();
+ s->for_each_neighbor([this, &queue](Square& neighbor) {
+ if (!neighbor.is_swept && !neighbor.has_mine && neighbor.number == 0) {
+ on_square_clicked_impl(neighbor, false);
+ queue.enqueue(&neighbor);
+ }
+ if (!neighbor.has_mine && neighbor.number)
+ on_square_clicked_impl(neighbor, false);
+ });
+ }
+}
+
+void Field::paint_event(GUI::PaintEvent& event)
+{
+ GUI::Frame::paint_event(event);
+ GUI::Painter painter(*this);
+ painter.add_clip_rect(event.rect());
+
+ auto inner_rect = frame_inner_rect();
+ painter.add_clip_rect(inner_rect);
+
+ for (int y = inner_rect.top() - 1; y <= inner_rect.bottom(); y += square_size()) {
+ Gfx::IntPoint a { inner_rect.left(), y };
+ Gfx::IntPoint b { inner_rect.right(), y };
+ painter.draw_line(a, b, palette().threed_shadow1());
+ }
+ for (int x = frame_inner_rect().left() - 1; x <= frame_inner_rect().right(); x += square_size()) {
+ Gfx::IntPoint a { x, inner_rect.top() };
+ Gfx::IntPoint b { x, inner_rect.bottom() };
+ painter.draw_line(a, b, palette().threed_shadow1());
+ }
+}
+
+void Field::on_square_clicked_impl(Square& square, bool should_flood_fill)
+{
+ if (m_first_click) {
+ while (square.has_mine || square.number != 0) {
+ reset();
+ }
+ }
+ m_first_click = false;
+
+ if (square.is_swept)
+ return;
+ if (square.has_flag)
+ return;
+ if (square.is_considering)
+ return;
+ if (!m_timer->is_active())
+ m_timer->start();
+ update();
+ square.is_swept = true;
+ square.button->set_visible(false);
+ square.label->set_visible(true);
+ if (square.has_mine) {
+ square.label->set_fill_with_background_color(true);
+ game_over();
+ return;
+ }
+
+ --m_unswept_empties;
+ if (should_flood_fill && square.number == 0)
+ flood_fill(square);
+
+ if (!m_unswept_empties)
+ win();
+}
+
+void Field::on_square_clicked(Square& square)
+{
+ on_square_clicked_impl(square, true);
+}
+
+void Field::on_square_chorded(Square& square)
+{
+ if (!square.is_swept)
+ return;
+ if (!square.number)
+ return;
+ size_t adjacent_flags = 0;
+ square.for_each_neighbor([&](auto& neighbor) {
+ if (neighbor.has_flag)
+ ++adjacent_flags;
+ });
+ if (square.number != adjacent_flags)
+ return;
+ square.for_each_neighbor([&](auto& neighbor) {
+ if (neighbor.has_flag)
+ return;
+ on_square_clicked(neighbor);
+ });
+}
+
+void Field::on_square_right_clicked(Square& square)
+{
+ if (square.is_swept)
+ return;
+ if (!square.has_flag && !m_flags_left)
+ return;
+
+ set_flag(square, !square.has_flag);
+}
+
+void Field::set_flag(Square& square, bool flag)
+{
+ ASSERT(!square.is_swept);
+ if (square.has_flag == flag)
+ return;
+ square.is_considering = false;
+
+ if (!flag) {
+ ++m_flags_left;
+ } else {
+
+ ASSERT(m_flags_left);
+ --m_flags_left;
+ }
+ square.has_flag = flag;
+
+ m_flag_label.set_text(String::number(m_flags_left));
+ square.button->set_icon(square.has_flag ? m_flag_bitmap : nullptr);
+ square.button->update();
+}
+
+void Field::on_square_middle_clicked(Square& square)
+{
+ if (square.is_swept)
+ return;
+ if (square.has_flag) {
+ ++m_flags_left;
+ square.has_flag = false;
+ m_flag_label.set_text(String::number(m_flags_left));
+ }
+ square.is_considering = !square.is_considering;
+ square.button->set_icon(square.is_considering ? m_consider_bitmap : nullptr);
+ square.button->update();
+}
+
+void Field::win()
+{
+ m_timer->stop();
+ set_greedy_for_hits(true);
+ set_face(Face::Good);
+ for_each_square([&](auto& square) {
+ if (!square.has_flag && square.has_mine)
+ set_flag(square, true);
+ });
+ reveal_mines();
+}
+
+void Field::game_over()
+{
+ m_timer->stop();
+ set_greedy_for_hits(true);
+ set_face(Face::Bad);
+ reveal_mines();
+}
+
+void Field::reveal_mines()
+{
+ for (size_t r = 0; r < rows(); ++r) {
+ for (size_t c = 0; c < columns(); ++c) {
+ auto& square = this->square(r, c);
+ if (square.has_mine && !square.has_flag) {
+ square.button->set_visible(false);
+ square.label->set_visible(true);
+ }
+ if (!square.has_mine && square.has_flag) {
+ square.button->set_icon(*m_badflag_bitmap);
+ square.button->set_visible(true);
+ square.label->set_visible(false);
+ }
+ }
+ }
+ update();
+}
+
+void Field::set_chord_preview(Square& square, bool chord_preview)
+{
+ if (m_chord_preview == chord_preview)
+ return;
+ m_chord_preview = chord_preview;
+ square.for_each_neighbor([&](auto& neighbor) {
+ neighbor.button->set_checked(false);
+ if (!neighbor.has_flag && !neighbor.is_considering)
+ neighbor.button->set_checked(chord_preview);
+ });
+}
+
+void Field::set_field_size(size_t rows, size_t columns, size_t mine_count)
+{
+ if (m_rows == rows && m_columns == columns && m_mine_count == mine_count)
+ return;
+ {
+ auto config = Core::ConfigFile::get_for_app("Minesweeper");
+ config->write_num_entry("Game", "MineCount", mine_count);
+ config->write_num_entry("Game", "Rows", rows);
+ config->write_num_entry("Game", "Columns", columns);
+ }
+ m_rows = rows;
+ m_columns = columns;
+ m_mine_count = mine_count;
+ set_fixed_size(frame_thickness() * 2 + m_columns * square_size(), frame_thickness() * 2 + m_rows * square_size());
+ reset();
+ m_on_size_changed(min_size());
+}
+
+void Field::set_single_chording(bool enabled)
+{
+ auto config = Core::ConfigFile::get_for_app("Minesweeper");
+ m_single_chording = enabled;
+ config->write_bool_entry("Minesweeper", "SingleChording", m_single_chording);
+}
+
+Square::Square()
+{
+}
+
+Square::~Square()
+{
+}
+
+template<typename Callback>
+void Field::for_each_square(Callback callback)
+{
+ for (size_t i = 0; i < rows() * columns(); ++i)
+ callback(*m_squares[i]);
+}
diff --git a/Userland/Games/Minesweeper/Field.h b/Userland/Games/Minesweeper/Field.h
new file mode 100644
index 0000000000..0ab7e0f300
--- /dev/null
+++ b/Userland/Games/Minesweeper/Field.h
@@ -0,0 +1,132 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include <AK/Noncopyable.h>
+#include <LibCore/Timer.h>
+#include <LibGUI/Frame.h>
+
+class Field;
+class SquareButton;
+class SquareLabel;
+
+class Square {
+ AK_MAKE_NONCOPYABLE(Square);
+
+public:
+ Square();
+ ~Square();
+
+ Field* field { nullptr };
+ bool is_swept { false };
+ bool has_mine { false };
+ bool has_flag { false };
+ bool is_considering { false };
+ size_t row { 0 };
+ size_t column { 0 };
+ size_t number { 0 };
+ RefPtr<SquareButton> button;
+ RefPtr<SquareLabel> label;
+
+ template<typename Callback>
+ void for_each_neighbor(Callback);
+};
+
+class Field final : public GUI::Frame {
+ C_OBJECT(Field)
+ friend class Square;
+ friend class SquareLabel;
+
+public:
+ Field(GUI::Label& flag_label, GUI::Label& time_label, GUI::Button& face_button, Function<void(Gfx::IntSize)> on_size_changed);
+ virtual ~Field() override;
+
+ size_t rows() const { return m_rows; }
+ size_t columns() const { return m_columns; }
+ size_t mine_count() const { return m_mine_count; }
+ int square_size() const { return 15; }
+ bool is_single_chording() const { return m_single_chording; }
+
+ void set_field_size(size_t rows, size_t columns, size_t mine_count);
+ void set_single_chording(bool new_val);
+
+ void reset();
+
+private:
+ virtual void paint_event(GUI::PaintEvent&) override;
+
+ void on_square_clicked(Square&);
+ void on_square_right_clicked(Square&);
+ void on_square_middle_clicked(Square&);
+ void on_square_chorded(Square&);
+ void game_over();
+ void win();
+ void reveal_mines();
+ void set_chord_preview(Square&, bool);
+ void set_flag(Square&, bool);
+
+ Square& square(size_t row, size_t column) { return *m_squares[row * columns() + column]; }
+ const Square& square(size_t row, size_t column) const { return *m_squares[row * columns() + column]; }
+
+ void flood_fill(Square&);
+ void on_square_clicked_impl(Square&, bool);
+
+ template<typename Callback>
+ void for_each_square(Callback);
+
+ enum class Face {
+ Default,
+ Good,
+ Bad
+ };
+ void set_face(Face);
+
+ size_t m_rows { 0 };
+ size_t m_columns { 0 };
+ size_t m_mine_count { 0 };
+ size_t m_unswept_empties { 0 };
+ Vector<OwnPtr<Square>> m_squares;
+ RefPtr<Gfx::Bitmap> m_mine_bitmap;
+ RefPtr<Gfx::Bitmap> m_flag_bitmap;
+ RefPtr<Gfx::Bitmap> m_badflag_bitmap;
+ RefPtr<Gfx::Bitmap> m_consider_bitmap;
+ RefPtr<Gfx::Bitmap> m_default_face_bitmap;
+ RefPtr<Gfx::Bitmap> m_good_face_bitmap;
+ RefPtr<Gfx::Bitmap> m_bad_face_bitmap;
+ RefPtr<Gfx::Bitmap> m_number_bitmap[8];
+ GUI::Button& m_face_button;
+ GUI::Label& m_flag_label;
+ GUI::Label& m_time_label;
+ RefPtr<Core::Timer> m_timer;
+ size_t m_time_elapsed { 0 };
+ size_t m_flags_left { 0 };
+ Face m_face { Face::Default };
+ bool m_chord_preview { false };
+ bool m_first_click { true };
+ bool m_single_chording { true };
+ Function<void(Gfx::IntSize)> m_on_size_changed;
+};
diff --git a/Userland/Games/Minesweeper/main.cpp b/Userland/Games/Minesweeper/main.cpp
new file mode 100644
index 0000000000..b4202ed867
--- /dev/null
+++ b/Userland/Games/Minesweeper/main.cpp
@@ -0,0 +1,152 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "Field.h"
+#include <LibCore/ConfigFile.h>
+#include <LibGUI/Action.h>
+#include <LibGUI/Application.h>
+#include <LibGUI/BoxLayout.h>
+#include <LibGUI/Button.h>
+#include <LibGUI/Icon.h>
+#include <LibGUI/ImageWidget.h>
+#include <LibGUI/Label.h>
+#include <LibGUI/Menu.h>
+#include <LibGUI/MenuBar.h>
+#include <LibGUI/Window.h>
+#include <stdio.h>
+
+int main(int argc, char** argv)
+{
+ if (pledge("stdio rpath accept wpath cpath shared_buffer unix fattr", nullptr) < 0) {
+ perror("pledge");
+ return 1;
+ }
+
+ auto app = GUI::Application::construct(argc, argv);
+
+ if (pledge("stdio rpath accept wpath cpath shared_buffer", nullptr) < 0) {
+ perror("pledge");
+ return 1;
+ }
+
+ auto config = Core::ConfigFile::get_for_app("Minesweeper");
+
+ if (unveil("/res", "r") < 0) {
+ perror("unveil");
+ return 1;
+ }
+
+ if (unveil(config->file_name().characters(), "crw") < 0) {
+ perror("unveil");
+ return 1;
+ }
+
+ if (unveil(nullptr, nullptr) < 0) {
+ perror("unveil");
+ return 1;
+ }
+
+ auto app_icon = GUI::Icon::default_icon("app-minesweeper");
+
+ auto window = GUI::Window::construct();
+ window->set_resizable(false);
+ window->set_title("Minesweeper");
+ window->resize(139, 175);
+
+ auto& widget = window->set_main_widget<GUI::Widget>();
+ widget.set_layout<GUI::VerticalBoxLayout>();
+ widget.layout()->set_spacing(0);
+
+ auto& container = widget.add<GUI::Widget>();
+ container.set_fill_with_background_color(true);
+ container.set_fixed_height(36);
+ container.set_layout<GUI::HorizontalBoxLayout>();
+
+ auto& flag_image = container.add<GUI::ImageWidget>();
+ flag_image.load_from_file("/res/icons/minesweeper/flag.png");
+
+ auto& flag_label = container.add<GUI::Label>();
+ auto& face_button = container.add<GUI::Button>();
+ face_button.set_button_style(Gfx::ButtonStyle::CoolBar);
+ face_button.set_fixed_width(36);
+
+ auto& time_image = container.add<GUI::ImageWidget>();
+ time_image.load_from_file("/res/icons/minesweeper/timer.png");
+
+ auto& time_label = container.add<GUI::Label>();
+ auto& field = widget.add<Field>(flag_label, time_label, face_button, [&](auto size) {
+ size.set_height(size.height() + container.min_size().height());
+ window->resize(size);
+ });
+
+ auto menubar = GUI::MenuBar::construct();
+
+ auto& app_menu = menubar->add_menu("Minesweeper");
+
+ app_menu.add_action(GUI::Action::create("New game", { Mod_None, Key_F2 }, [&](auto&) {
+ field.reset();
+ }));
+
+ app_menu.add_separator();
+
+ auto chord_toggler_action = GUI::Action::create_checkable("Single-click chording", [&](auto& action) {
+ field.set_single_chording(action.is_checked());
+ });
+ chord_toggler_action->set_checked(field.is_single_chording());
+
+ app_menu.add_action(*chord_toggler_action);
+ app_menu.add_separator();
+
+ app_menu.add_action(GUI::CommonActions::make_quit_action([](auto&) {
+ GUI::Application::the()->quit();
+ return;
+ }));
+
+ auto& difficulty_menu = menubar->add_menu("Difficulty");
+ difficulty_menu.add_action(GUI::Action::create("Beginner", { Mod_Ctrl, Key_B }, [&](auto&) {
+ field.set_field_size(9, 9, 10);
+ }));
+ difficulty_menu.add_action(GUI::Action::create("Intermediate", { Mod_Ctrl, Key_I }, [&](auto&) {
+ field.set_field_size(16, 16, 40);
+ }));
+ difficulty_menu.add_action(GUI::Action::create("Expert", { Mod_Ctrl, Key_E }, [&](auto&) {
+ field.set_field_size(16, 30, 99);
+ }));
+ difficulty_menu.add_action(GUI::Action::create("Madwoman", { Mod_Ctrl, Key_M }, [&](auto&) {
+ field.set_field_size(32, 60, 350);
+ }));
+
+ auto& help_menu = menubar->add_menu("Help");
+ help_menu.add_action(GUI::CommonActions::make_about_action("Minesweeper", app_icon, window));
+
+ app->set_menubar(move(menubar));
+
+ window->show();
+
+ window->set_icon(app_icon.bitmap_for_size(16));
+
+ return app->exec();
+}
diff --git a/Userland/Games/Pong/CMakeLists.txt b/Userland/Games/Pong/CMakeLists.txt
new file mode 100644
index 0000000000..ebd8962f94
--- /dev/null
+++ b/Userland/Games/Pong/CMakeLists.txt
@@ -0,0 +1,7 @@
+set(SOURCES
+ main.cpp
+ Game.cpp
+)
+
+serenity_app(Pong ICON app-pong)
+target_link_libraries(Pong LibGUI)
diff --git a/Userland/Games/Pong/Game.cpp b/Userland/Games/Pong/Game.cpp
new file mode 100644
index 0000000000..f028bf914e
--- /dev/null
+++ b/Userland/Games/Pong/Game.cpp
@@ -0,0 +1,236 @@
+/*
+ * Copyright (c) 2020, the SerenityOS developers.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "Game.h"
+
+namespace Pong {
+
+Game::Game()
+{
+ set_override_cursor(Gfx::StandardCursor::Hidden);
+ start_timer(16);
+ reset();
+}
+
+Game::~Game()
+{
+}
+
+void Game::reset_paddles()
+{
+ m_player1_paddle.moving_up = false;
+ m_player1_paddle.moving_down = false;
+ m_player1_paddle.rect = { game_width - 12, game_height / 2 - 40, m_player1_paddle.width, m_player1_paddle.height };
+ m_player2_paddle.moving_up = false;
+ m_player2_paddle.moving_down = false;
+ m_player2_paddle.rect = { 4, game_height / 2 - 40, m_player2_paddle.width, m_player2_paddle.height };
+}
+
+void Game::reset()
+{
+ reset_ball(1);
+ reset_paddles();
+}
+
+void Game::timer_event(Core::TimerEvent&)
+{
+ tick();
+}
+
+void Game::paint_event(GUI::PaintEvent& event)
+{
+ GUI::Painter painter(*this);
+ painter.add_clip_rect(event.rect());
+
+ painter.fill_rect(rect(), Color::Black);
+ painter.fill_rect(enclosing_int_rect(m_net.rect()), m_net.color);
+
+ painter.fill_ellipse(enclosing_int_rect(m_ball.rect()), Color::Red);
+
+ painter.fill_rect(enclosing_int_rect(m_player1_paddle.rect), m_player1_paddle.color);
+ painter.fill_rect(enclosing_int_rect(m_player2_paddle.rect), m_player2_paddle.color);
+
+ painter.draw_text(player_1_score_rect(), String::formatted("{}", m_player_1_score), Gfx::TextAlignment::TopLeft, Color::White);
+ painter.draw_text(player_2_score_rect(), String::formatted("{}", m_player_2_score), Gfx::TextAlignment::TopLeft, Color::White);
+}
+
+void Game::keyup_event(GUI::KeyEvent& event)
+{
+ switch (event.key()) {
+ case Key_Up:
+ m_player1_paddle.moving_up = false;
+ break;
+ case Key_Down:
+ m_player1_paddle.moving_down = false;
+ break;
+ default:
+ break;
+ }
+}
+
+void Game::keydown_event(GUI::KeyEvent& event)
+{
+ switch (event.key()) {
+ case Key_Escape:
+ GUI::Application::the()->quit();
+ break;
+ case Key_Up:
+ m_player1_paddle.moving_up = true;
+ break;
+ case Key_Down:
+ m_player1_paddle.moving_down = true;
+ break;
+ default:
+ break;
+ }
+}
+
+void Game::mousemove_event(GUI::MouseEvent& event)
+{
+ float new_paddle_y = event.y() - m_player1_paddle.rect.height() / 2;
+ new_paddle_y = max(0.0f, new_paddle_y);
+ new_paddle_y = min(game_height - m_player1_paddle.rect.height(), new_paddle_y);
+ m_player1_paddle.rect.set_y(new_paddle_y);
+}
+
+void Game::reset_ball(int serve_to_player)
+{
+ int position_y_min = (game_width / 2) - 50;
+ int position_y_max = (game_width / 2) + 50;
+ int position_y = arc4random() % (position_y_max - position_y_min + 1) + position_y_min;
+ int position_x = (game_height / 2);
+ int velocity_y = arc4random() % 3 + 1;
+ int velocity_x = 5 + (5 - velocity_y);
+ if (arc4random() % 2)
+ velocity_y = velocity_y * -1;
+ if (serve_to_player == 2)
+ velocity_x = velocity_x * -1;
+
+ m_ball = {};
+ m_ball.position = { position_x, position_y };
+ m_ball.velocity = { velocity_x, velocity_y };
+}
+
+void Game::game_over(int winner)
+{
+ GUI::MessageBox::show(window(), String::format("Player %d wins!", winner), "Pong", GUI::MessageBox::Type::Warning, GUI::MessageBox::InputType::OK);
+}
+
+void Game::round_over(int winner)
+{
+ stop_timer();
+ if (winner == 1)
+ m_player_1_score++;
+
+ if (winner == 2)
+ m_player_2_score++;
+
+ if (m_player_1_score == m_score_to_win || m_player_2_score == m_score_to_win) {
+ game_over(winner);
+ return;
+ }
+
+ reset_ball(winner);
+ reset_paddles();
+ start_timer(16);
+}
+
+void Game::calculate_move()
+{
+ if ((m_ball.y() + m_ball.radius) < (m_player2_paddle.rect.y() + (m_player2_paddle.rect.height() / 2))) {
+ m_player2_paddle.moving_up = true;
+ m_player2_paddle.moving_down = false;
+ return;
+ }
+ if ((m_ball.y() + m_ball.radius) > (m_player2_paddle.rect.y() + (m_player2_paddle.rect.height() / 2))) {
+ m_player2_paddle.moving_up = false;
+ m_player2_paddle.moving_down = true;
+ return;
+ }
+ m_player2_paddle.moving_up = false;
+ m_player2_paddle.moving_down = false;
+}
+
+void Game::tick()
+{
+ auto new_ball = m_ball;
+ new_ball.position += new_ball.velocity;
+
+ if (new_ball.y() < new_ball.radius || new_ball.y() > game_height - new_ball.radius) {
+ new_ball.position.set_y(m_ball.y());
+ new_ball.velocity.set_y(new_ball.velocity.y() * -1);
+ }
+
+ if (new_ball.x() < new_ball.radius) {
+ round_over(1);
+ return;
+ }
+
+ if (new_ball.x() > (game_width - new_ball.radius)) {
+ round_over(2);
+ return;
+ }
+
+ if (new_ball.rect().intersects(m_player1_paddle.rect)) {
+ new_ball.position.set_x(m_ball.x());
+ new_ball.velocity.set_x(new_ball.velocity.x() * -1);
+
+ float distance_to_middle_of_paddle = new_ball.y() - m_player1_paddle.rect.center().y();
+ float relative_impact_point = distance_to_middle_of_paddle / m_player1_paddle.rect.height();
+ new_ball.velocity.set_y(relative_impact_point * 7);
+ }
+
+ if (new_ball.rect().intersects(m_player2_paddle.rect)) {
+ new_ball.position.set_x(m_ball.x());
+ new_ball.velocity.set_x(new_ball.velocity.x() * -1);
+
+ float distance_to_middle_of_paddle = new_ball.y() - m_player2_paddle.rect.center().y();
+ float relative_impact_point = distance_to_middle_of_paddle / m_player2_paddle.rect.height();
+ new_ball.velocity.set_y(relative_impact_point * 7);
+ }
+
+ if (m_player1_paddle.moving_up) {
+ m_player1_paddle.rect.set_y(max(0.0f, m_player1_paddle.rect.y() - m_player1_paddle.speed));
+ }
+ if (m_player1_paddle.moving_down) {
+ m_player1_paddle.rect.set_y(min(game_height - m_player1_paddle.rect.height(), m_player1_paddle.rect.y() + m_player1_paddle.speed));
+ }
+
+ calculate_move();
+
+ if (m_player2_paddle.moving_up) {
+ m_player2_paddle.rect.set_y(max(0.0f, m_player2_paddle.rect.y() - m_player2_paddle.speed));
+ }
+ if (m_player2_paddle.moving_down) {
+ m_player2_paddle.rect.set_y(min(game_height - m_player2_paddle.rect.height(), m_player2_paddle.rect.y() + m_player2_paddle.speed));
+ }
+
+ m_ball = new_ball;
+
+ update();
+}
+
+}
diff --git a/Userland/Games/Pong/Game.h b/Userland/Games/Pong/Game.h
new file mode 100644
index 0000000000..df042f0be7
--- /dev/null
+++ b/Userland/Games/Pong/Game.h
@@ -0,0 +1,120 @@
+/*
+ * Copyright (c) 2020, the SerenityOS developers.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include <LibCore/ConfigFile.h>
+#include <LibGUI/Application.h>
+#include <LibGUI/MessageBox.h>
+#include <LibGUI/Painter.h>
+#include <LibGUI/Widget.h>
+#include <LibGfx/Bitmap.h>
+#include <LibGfx/Font.h>
+#include <LibGfx/StandardCursor.h>
+
+namespace Pong {
+
+class Game final : public GUI::Widget {
+ C_OBJECT(Game);
+
+public:
+ static const int game_width = 560;
+ static const int game_height = 480;
+
+ virtual ~Game() override;
+
+private:
+ Game();
+
+ virtual void paint_event(GUI::PaintEvent&) override;
+ virtual void keyup_event(GUI::KeyEvent&) override;
+ virtual void keydown_event(GUI::KeyEvent&) override;
+ virtual void mousemove_event(GUI::MouseEvent&) override;
+ virtual void timer_event(Core::TimerEvent&) override;
+
+ void reset();
+ void reset_ball(int serve_to_player);
+ void reset_paddles();
+ void tick();
+ void round_over(int player);
+ void game_over(int player);
+ void calculate_move();
+
+ struct Ball {
+ Gfx::FloatPoint position;
+ Gfx::FloatPoint velocity;
+ float radius { 4 };
+
+ float x() const { return position.x(); }
+ float y() const { return position.y(); }
+
+ Gfx::FloatRect rect() const
+ {
+ return { x() - radius, y() - radius, radius * 2, radius * 2 };
+ }
+ };
+
+ struct Paddle {
+ Gfx::FloatRect rect;
+ float speed { 5 };
+ float width { 8 };
+ float height { 28 };
+ bool moving_up { false };
+ bool moving_down { false };
+ Gfx::Color color { Color::White };
+ };
+
+ struct Net {
+ Gfx::Color color { Color::White };
+ Gfx::FloatRect rect() const
+ {
+ return { (game_width / 2) - 1, 0, 2, game_height };
+ }
+ };
+
+ Gfx::IntRect player_1_score_rect() const
+ {
+ int score_width = font().width(String::formatted("{}", m_player_1_score));
+ return { (game_width / 2) + score_width + 2, 2, score_width, font().glyph_height() };
+ }
+
+ Gfx::IntRect player_2_score_rect() const
+ {
+ int score_width = font().width(String::formatted("{}", m_player_2_score));
+ return { (game_width / 2) - score_width - 2, 2, score_width, font().glyph_height() };
+ }
+
+ Net m_net;
+ Ball m_ball;
+ Paddle m_player1_paddle;
+ Paddle m_player2_paddle;
+
+ int m_score_to_win = 21;
+ int m_player_1_score = 0;
+ int m_player_2_score = 0;
+};
+
+}
diff --git a/Userland/Games/Pong/main.cpp b/Userland/Games/Pong/main.cpp
new file mode 100644
index 0000000000..f4b7c235f1
--- /dev/null
+++ b/Userland/Games/Pong/main.cpp
@@ -0,0 +1,90 @@
+/*
+ * Copyright (c) 2020, the SerenityOS developers.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "Game.h"
+#include <LibCore/ConfigFile.h>
+#include <LibGUI/Application.h>
+#include <LibGUI/Icon.h>
+#include <LibGUI/Menu.h>
+#include <LibGUI/MenuBar.h>
+#include <LibGUI/Window.h>
+#include <LibGfx/Bitmap.h>
+
+int main(int argc, char** argv)
+{
+ if (pledge("stdio rpath wpath cpath shared_buffer accept cpath unix fattr", nullptr) < 0) {
+ perror("pledge");
+ return 1;
+ }
+
+ auto app = GUI::Application::construct(argc, argv);
+
+ if (pledge("stdio rpath wpath cpath shared_buffer accept", nullptr) < 0) {
+ perror("pledge");
+ return 1;
+ }
+
+ auto config = Core::ConfigFile::get_for_app("Pong");
+
+ if (unveil("/res", "r") < 0) {
+ perror("unveil");
+ return 1;
+ }
+
+ if (unveil(config->file_name().characters(), "rwc") < 0) {
+ perror("unveil");
+ return 1;
+ }
+
+ if (unveil(nullptr, nullptr) < 0) {
+ perror("unveil");
+ return 1;
+ }
+
+ auto window = GUI::Window::construct();
+ window->resize(Pong::Game::game_width, Pong::Game::game_height);
+ auto app_icon = GUI::Icon::default_icon("app-pong");
+ window->set_icon(app_icon.bitmap_for_size(16));
+ window->set_title("Pong");
+ window->set_double_buffering_enabled(false);
+ window->set_main_widget<Pong::Game>();
+ window->show();
+
+ auto menubar = GUI::MenuBar::construct();
+
+ auto& app_menu = menubar->add_menu("Pong");
+ app_menu.add_action(GUI::CommonActions::make_quit_action([](auto&) {
+ GUI::Application::the()->quit();
+ return;
+ }));
+
+ auto& help_menu = menubar->add_menu("Help");
+ help_menu.add_action(GUI::CommonActions::make_about_action("Pong", app_icon, window));
+
+ app->set_menubar(move(menubar));
+
+ return app->exec();
+}
diff --git a/Userland/Games/Snake/CMakeLists.txt b/Userland/Games/Snake/CMakeLists.txt
new file mode 100644
index 0000000000..d542cd4ead
--- /dev/null
+++ b/Userland/Games/Snake/CMakeLists.txt
@@ -0,0 +1,7 @@
+set(SOURCES
+ main.cpp
+ SnakeGame.cpp
+)
+
+serenity_app(Snake ICON app-snake)
+target_link_libraries(Snake LibGUI)
diff --git a/Userland/Games/Snake/SnakeGame.cpp b/Userland/Games/Snake/SnakeGame.cpp
new file mode 100644
index 0000000000..4c96a7aee9
--- /dev/null
+++ b/Userland/Games/Snake/SnakeGame.cpp
@@ -0,0 +1,257 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "SnakeGame.h"
+#include <LibCore/ConfigFile.h>
+#include <LibGUI/Painter.h>
+#include <LibGfx/Bitmap.h>
+#include <LibGfx/Font.h>
+#include <LibGfx/FontDatabase.h>
+#include <stdlib.h>
+#include <time.h>
+
+SnakeGame::SnakeGame()
+{
+ set_font(Gfx::FontDatabase::default_bold_fixed_width_font());
+ m_fruit_bitmaps.append(*Gfx::Bitmap::load_from_file("/res/icons/snake/paprika.png"));
+ m_fruit_bitmaps.append(*Gfx::Bitmap::load_from_file("/res/icons/snake/eggplant.png"));
+ m_fruit_bitmaps.append(*Gfx::Bitmap::load_from_file("/res/icons/snake/cauliflower.png"));
+ m_fruit_bitmaps.append(*Gfx::Bitmap::load_from_file("/res/icons/snake/tomato.png"));
+ srand(time(nullptr));
+ reset();
+
+ auto config = Core::ConfigFile::get_for_app("Snake");
+ m_high_score = config->read_num_entry("Snake", "HighScore", 0);
+ m_high_score_text = String::formatted("Best: {}", m_high_score);
+}
+
+SnakeGame::~SnakeGame()
+{
+}
+
+void SnakeGame::reset()
+{
+ m_head = { m_rows / 2, m_columns / 2 };
+ m_tail.clear_with_capacity();
+ m_length = 2;
+ m_score = 0;
+ m_score_text = "Score: 0";
+ m_velocity_queue.clear();
+ stop_timer();
+ start_timer(100);
+ spawn_fruit();
+ update();
+}
+
+bool SnakeGame::is_available(const Coordinate& coord)
+{
+ for (size_t i = 0; i < m_tail.size(); ++i) {
+ if (m_tail[i] == coord)
+ return false;
+ }
+ if (m_head == coord)
+ return false;
+ if (m_fruit == coord)
+ return false;
+ return true;
+}
+
+void SnakeGame::spawn_fruit()
+{
+ Coordinate coord;
+ for (;;) {
+ coord.row = rand() % m_rows;
+ coord.column = rand() % m_columns;
+ if (is_available(coord))
+ break;
+ }
+ m_fruit = coord;
+ m_fruit_type = rand() % m_fruit_bitmaps.size();
+}
+
+Gfx::IntRect SnakeGame::score_rect() const
+{
+ int score_width = font().width(m_score_text);
+ return { width() - score_width - 2, height() - font().glyph_height() - 2, score_width, font().glyph_height() };
+}
+
+Gfx::IntRect SnakeGame::high_score_rect() const
+{
+ int high_score_width = font().width(m_high_score_text);
+ return { 2, height() - font().glyph_height() - 2, high_score_width, font().glyph_height() };
+}
+
+void SnakeGame::timer_event(Core::TimerEvent&)
+{
+ Vector<Coordinate> dirty_cells;
+
+ m_tail.prepend(m_head);
+
+ if (m_tail.size() > m_length) {
+ dirty_cells.append(m_tail.last());
+ m_tail.take_last();
+ }
+
+ if (!m_velocity_queue.is_empty())
+ m_velocity = m_velocity_queue.dequeue();
+
+ dirty_cells.append(m_head);
+
+ m_head.row += m_velocity.vertical;
+ m_head.column += m_velocity.horizontal;
+
+ m_last_velocity = m_velocity;
+
+ if (m_head.row >= m_rows)
+ m_head.row = 0;
+ if (m_head.row < 0)
+ m_head.row = m_rows - 1;
+ if (m_head.column >= m_columns)
+ m_head.column = 0;
+ if (m_head.column < 0)
+ m_head.column = m_columns - 1;
+
+ dirty_cells.append(m_head);
+
+ for (size_t i = 0; i < m_tail.size(); ++i) {
+ if (m_head == m_tail[i]) {
+ game_over();
+ return;
+ }
+ }
+
+ if (m_head == m_fruit) {
+ ++m_length;
+ ++m_score;
+ m_score_text = String::formatted("Score: {}", m_score);
+ if (m_score > m_high_score) {
+ m_high_score = m_score;
+ m_high_score_text = String::formatted("Best: {}", m_high_score);
+ update(high_score_rect());
+ auto config = Core::ConfigFile::get_for_app("Snake");
+ config->write_num_entry("Snake", "HighScore", m_high_score);
+ }
+ update(score_rect());
+ dirty_cells.append(m_fruit);
+ spawn_fruit();
+ dirty_cells.append(m_fruit);
+ }
+
+ for (auto& coord : dirty_cells) {
+ update(cell_rect(coord));
+ }
+}
+
+void SnakeGame::keydown_event(GUI::KeyEvent& event)
+{
+ switch (event.key()) {
+ case KeyCode::Key_A:
+ case KeyCode::Key_Left:
+ if (last_velocity().horizontal == 1)
+ break;
+ queue_velocity(0, -1);
+ break;
+ case KeyCode::Key_D:
+ case KeyCode::Key_Right:
+ if (last_velocity().horizontal == -1)
+ break;
+ queue_velocity(0, 1);
+ break;
+ case KeyCode::Key_W:
+ case KeyCode::Key_Up:
+ if (last_velocity().vertical == 1)
+ break;
+ queue_velocity(-1, 0);
+ break;
+ case KeyCode::Key_S:
+ case KeyCode::Key_Down:
+ if (last_velocity().vertical == -1)
+ break;
+ queue_velocity(1, 0);
+ break;
+ default:
+ break;
+ }
+}
+
+Gfx::IntRect SnakeGame::cell_rect(const Coordinate& coord) const
+{
+ auto game_rect = rect();
+ auto cell_size = Gfx::IntSize(game_rect.width() / m_columns, game_rect.height() / m_rows);
+ return {
+ coord.column * cell_size.width(),
+ coord.row * cell_size.height(),
+ cell_size.width(),
+ cell_size.height()
+ };
+}
+
+void SnakeGame::paint_event(GUI::PaintEvent& event)
+{
+ GUI::Painter painter(*this);
+ painter.add_clip_rect(event.rect());
+ painter.fill_rect(event.rect(), Color::Black);
+
+ painter.fill_rect(cell_rect(m_head), Color::Yellow);
+ for (auto& part : m_tail) {
+ auto rect = cell_rect(part);
+ painter.fill_rect(rect, Color::from_rgb(0xaaaa00));
+
+ Gfx::IntRect left_side(rect.x(), rect.y(), 2, rect.height());
+ Gfx::IntRect top_side(rect.x(), rect.y(), rect.width(), 2);
+ Gfx::IntRect right_side(rect.right() - 1, rect.y(), 2, rect.height());
+ Gfx::IntRect bottom_side(rect.x(), rect.bottom() - 1, rect.width(), 2);
+ painter.fill_rect(left_side, Color::from_rgb(0xcccc00));
+ painter.fill_rect(right_side, Color::from_rgb(0x888800));
+ painter.fill_rect(top_side, Color::from_rgb(0xcccc00));
+ painter.fill_rect(bottom_side, Color::from_rgb(0x888800));
+ }
+
+ painter.draw_scaled_bitmap(cell_rect(m_fruit), m_fruit_bitmaps[m_fruit_type], m_fruit_bitmaps[m_fruit_type].rect());
+
+ painter.draw_text(high_score_rect(), m_high_score_text, Gfx::TextAlignment::TopLeft, Color::from_rgb(0xfafae0));
+ painter.draw_text(score_rect(), m_score_text, Gfx::TextAlignment::TopLeft, Color::White);
+}
+
+void SnakeGame::game_over()
+{
+ reset();
+}
+
+void SnakeGame::queue_velocity(int v, int h)
+{
+ if (last_velocity().vertical == v && last_velocity().horizontal == h)
+ return;
+ m_velocity_queue.enqueue({ v, h });
+}
+
+const SnakeGame::Velocity& SnakeGame::last_velocity() const
+{
+ if (!m_velocity_queue.is_empty())
+ return m_velocity_queue.last();
+
+ return m_last_velocity;
+}
diff --git a/Userland/Games/Snake/SnakeGame.h b/Userland/Games/Snake/SnakeGame.h
new file mode 100644
index 0000000000..790fc75746
--- /dev/null
+++ b/Userland/Games/Snake/SnakeGame.h
@@ -0,0 +1,91 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include <AK/CircularQueue.h>
+#include <AK/NonnullRefPtrVector.h>
+#include <LibGUI/Widget.h>
+
+class SnakeGame : public GUI::Widget {
+ C_OBJECT(SnakeGame)
+public:
+ virtual ~SnakeGame() override;
+
+ void reset();
+
+private:
+ SnakeGame();
+ virtual void paint_event(GUI::PaintEvent&) override;
+ virtual void keydown_event(GUI::KeyEvent&) override;
+ virtual void timer_event(Core::TimerEvent&) override;
+
+ struct Coordinate {
+ int row { 0 };
+ int column { 0 };
+
+ bool operator==(const Coordinate& other) const
+ {
+ return row == other.row && column == other.column;
+ }
+ };
+
+ struct Velocity {
+ int vertical { 0 };
+ int horizontal { 0 };
+ };
+
+ void game_over();
+ void spawn_fruit();
+ bool is_available(const Coordinate&);
+ void queue_velocity(int v, int h);
+ const Velocity& last_velocity() const;
+ Gfx::IntRect cell_rect(const Coordinate&) const;
+ Gfx::IntRect score_rect() const;
+ Gfx::IntRect high_score_rect() const;
+
+ int m_rows { 20 };
+ int m_columns { 20 };
+
+ Velocity m_velocity { 0, 1 };
+ Velocity m_last_velocity { 0, 1 };
+
+ CircularQueue<Velocity, 10> m_velocity_queue;
+
+ Coordinate m_head;
+ Vector<Coordinate> m_tail;
+
+ Coordinate m_fruit;
+ int m_fruit_type { 0 };
+
+ size_t m_length { 0 };
+ unsigned m_score { 0 };
+ String m_score_text;
+ unsigned m_high_score { 0 };
+ String m_high_score_text;
+
+ NonnullRefPtrVector<Gfx::Bitmap> m_fruit_bitmaps;
+};
diff --git a/Userland/Games/Snake/main.cpp b/Userland/Games/Snake/main.cpp
new file mode 100644
index 0000000000..220f44e87e
--- /dev/null
+++ b/Userland/Games/Snake/main.cpp
@@ -0,0 +1,102 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "SnakeGame.h"
+#include <LibCore/ConfigFile.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 <stdio.h>
+
+int main(int argc, char** argv)
+{
+ if (pledge("stdio rpath wpath cpath shared_buffer accept cpath unix fattr", nullptr) < 0) {
+ perror("pledge");
+ return 1;
+ }
+
+ auto app = GUI::Application::construct(argc, argv);
+
+ if (pledge("stdio rpath wpath cpath shared_buffer accept", nullptr) < 0) {
+ perror("pledge");
+ return 1;
+ }
+
+ auto config = Core::ConfigFile::get_for_app("Snake");
+
+ if (unveil("/res", "r") < 0) {
+ perror("unveil");
+ return 1;
+ }
+
+ if (unveil(config->file_name().characters(), "crw") < 0) {
+ perror("unveil");
+ return 1;
+ }
+
+ if (unveil(nullptr, nullptr) < 0) {
+ perror("unveil");
+ return 1;
+ }
+
+ auto app_icon = GUI::Icon::default_icon("app-snake");
+
+ auto window = GUI::Window::construct();
+
+ window->set_double_buffering_enabled(false);
+ window->set_title("Snake");
+ window->resize(320, 320);
+
+ auto& game = window->set_main_widget<SnakeGame>();
+
+ auto menubar = GUI::MenuBar::construct();
+
+ auto& app_menu = menubar->add_menu("Snake");
+
+ app_menu.add_action(GUI::Action::create("New game", { Mod_None, Key_F2 }, [&](auto&) {
+ game.reset();
+ }));
+ app_menu.add_separator();
+ app_menu.add_action(GUI::CommonActions::make_quit_action([](auto&) {
+ GUI::Application::the()->quit();
+ }));
+
+ auto& help_menu = menubar->add_menu("Help");
+ help_menu.add_action(GUI::CommonActions::make_about_action("Snake", app_icon, window));
+
+ app->set_menubar(move(menubar));
+
+ window->show();
+
+ window->set_icon(app_icon.bitmap_for_size(16));
+
+ return app->exec();
+}
diff --git a/Userland/Games/Solitaire/CMakeLists.txt b/Userland/Games/Solitaire/CMakeLists.txt
new file mode 100644
index 0000000000..4a9d4e6c71
--- /dev/null
+++ b/Userland/Games/Solitaire/CMakeLists.txt
@@ -0,0 +1,9 @@
+set(SOURCES
+ Card.cpp
+ CardStack.cpp
+ main.cpp
+ SolitaireWidget.cpp
+)
+
+serenity_app(Solitaire ICON app-solitaire)
+target_link_libraries(Solitaire LibGUI LibGfx LibCore)
diff --git a/Userland/Games/Solitaire/Card.cpp b/Userland/Games/Solitaire/Card.cpp
new file mode 100644
index 0000000000..23c3473632
--- /dev/null
+++ b/Userland/Games/Solitaire/Card.cpp
@@ -0,0 +1,177 @@
+/*
+ * Copyright (c) 2020, Till Mayer <till.mayer@web.de>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "Card.h"
+#include <LibGUI/Widget.h>
+#include <LibGfx/Font.h>
+#include <LibGfx/FontDatabase.h>
+
+static const NonnullRefPtr<Gfx::CharacterBitmap> s_diamond = Gfx::CharacterBitmap::create_from_ascii(
+ " # "
+ " ### "
+ " ##### "
+ " ####### "
+ "#########"
+ " ####### "
+ " ##### "
+ " ### "
+ " # ",
+ 9, 9);
+
+static const NonnullRefPtr<Gfx::CharacterBitmap> s_heart = Gfx::CharacterBitmap::create_from_ascii(
+ " # # "
+ " ### ### "
+ "#########"
+ "#########"
+ "#########"
+ " ####### "
+ " ##### "
+ " ### "
+ " # ",
+ 9, 9);
+
+static const NonnullRefPtr<Gfx::CharacterBitmap> s_spade = Gfx::CharacterBitmap::create_from_ascii(
+ " # "
+ " ### "
+ " ##### "
+ " ####### "
+ "#########"
+ "#########"
+ " ## # ## "
+ " ### "
+ " ### ",
+ 9, 9);
+
+static const NonnullRefPtr<Gfx::CharacterBitmap> s_club = Gfx::CharacterBitmap::create_from_ascii(
+ " ### "
+ " ##### "
+ " ##### "
+ " ## ### ## "
+ "###########"
+ "###########"
+ "#### # ####"
+ " ## ### ## "
+ " ### ",
+ 11, 9);
+
+static RefPtr<Gfx::Bitmap> s_background;
+
+Card::Card(Type type, uint8_t value)
+ : m_rect(Gfx::IntRect({}, { width, height }))
+ , m_front(*Gfx::Bitmap::create(Gfx::BitmapFormat::RGB32, { width, height }))
+ , m_type(type)
+ , m_value(value)
+{
+ ASSERT(value < card_count);
+ Gfx::IntRect paint_rect({ 0, 0 }, { width, height });
+
+ if (s_background.is_null()) {
+ s_background = Gfx::Bitmap::create(Gfx::BitmapFormat::RGB32, { width, height });
+ Gfx::Painter bg_painter(*s_background);
+
+ s_background->fill(Color::White);
+ auto image = Gfx::Bitmap::load_from_file("/res/icons/solitaire/buggie-deck.png");
+ ASSERT(!image.is_null());
+
+ float aspect_ratio = image->width() / static_cast<float>(image->height());
+ auto target_size = Gfx::IntSize(static_cast<int>(aspect_ratio * (height - 5)), height - 5);
+
+ bg_painter.draw_scaled_bitmap(
+ { { (width - target_size.width()) / 2, (height - target_size.height()) / 2 }, target_size },
+ *image, image->rect());
+ bg_painter.draw_rect(paint_rect, Color::Black);
+ }
+
+ Gfx::Painter painter(m_front);
+ auto& font = Gfx::FontDatabase::default_bold_font();
+ static const String labels[] = { "A", "2", "3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K" };
+
+ auto label = labels[value];
+ m_front->fill(Color::White);
+ painter.draw_rect(paint_rect, Color::Black);
+ paint_rect.set_height(paint_rect.height() / 2);
+ paint_rect.shrink(10, 6);
+
+ painter.draw_text(paint_rect, label, font, Gfx::TextAlignment::TopLeft, color());
+
+ NonnullRefPtr<Gfx::CharacterBitmap> symbol = s_diamond;
+ switch (m_type) {
+ case Diamonds:
+ symbol = s_diamond;
+ break;
+ case Clubs:
+ symbol = s_club;
+ break;
+ case Spades:
+ symbol = s_spade;
+ break;
+ case Hearts:
+ symbol = s_heart;
+ break;
+ default:
+ ASSERT_NOT_REACHED();
+ }
+
+ painter.draw_bitmap(
+ { paint_rect.x() + (font.width(label) - symbol->size().width()) / 2, font.glyph_height() + paint_rect.y() + 3 },
+ symbol, color());
+
+ for (int y = height / 2; y < height; ++y) {
+ for (int x = 0; x < width; ++x) {
+ m_front->set_pixel(x, y, m_front->get_pixel(width - x - 1, height - y - 1));
+ }
+ }
+}
+
+Card::~Card()
+{
+}
+
+void Card::draw(GUI::Painter& painter) const
+{
+ ASSERT(!s_background.is_null());
+ painter.blit(position(), m_upside_down ? *s_background : *m_front, m_front->rect());
+}
+
+void Card::clear(GUI::Painter& painter, const Color& background_color) const
+{
+ painter.fill_rect({ old_positon(), { width, height } }, background_color);
+}
+
+void Card::save_old_position()
+{
+ m_old_position = m_rect.location();
+ m_old_position_valid = true;
+}
+
+void Card::clear_and_draw(GUI::Painter& painter, const Color& background_color)
+{
+ if (is_old_position_valid())
+ clear(painter, background_color);
+
+ draw(painter);
+ save_old_position();
+}
diff --git a/Userland/Games/Solitaire/Card.h b/Userland/Games/Solitaire/Card.h
new file mode 100644
index 0000000000..8a5652d481
--- /dev/null
+++ b/Userland/Games/Solitaire/Card.h
@@ -0,0 +1,85 @@
+/*
+ * Copyright (c) 2020, Till Mayer <till.mayer@web.de>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include <LibCore/Object.h>
+#include <LibGUI/Painter.h>
+#include <LibGfx/Bitmap.h>
+#include <LibGfx/CharacterBitmap.h>
+#include <LibGfx/Rect.h>
+#include <ctype.h>
+
+class Card final : public Core::Object {
+ C_OBJECT(Card)
+public:
+ static constexpr int width = 80;
+ static constexpr int height = 100;
+ static constexpr int card_count = 13;
+
+ enum Type {
+ Clubs,
+ Diamonds,
+ Hearts,
+ Spades,
+ __Count
+ };
+
+ virtual ~Card() override;
+
+ Gfx::IntRect& rect() { return m_rect; }
+ Gfx::IntPoint position() const { return m_rect.location(); }
+ const Gfx::IntPoint& old_positon() const { return m_old_position; }
+ uint8_t value() const { return m_value; };
+ Type type() const { return m_type; }
+
+ bool is_old_position_valid() const { return m_old_position_valid; }
+ bool is_moving() const { return m_moving; }
+ bool is_upside_down() const { return m_upside_down; }
+ Gfx::Color color() const { return (m_type == Diamonds || m_type == Hearts) ? Color::Red : Color::Black; }
+
+ void set_position(const Gfx::IntPoint p) { m_rect.set_location(p); }
+ void set_moving(bool moving) { m_moving = moving; }
+ void set_upside_down(bool upside_down) { m_upside_down = upside_down; }
+
+ void save_old_position();
+
+ void draw(GUI::Painter&) const;
+ void clear(GUI::Painter&, const Color& background_color) const;
+ void clear_and_draw(GUI::Painter&, const Color& background_color);
+
+private:
+ Card(Type type, uint8_t value);
+
+ Gfx::IntRect m_rect;
+ NonnullRefPtr<Gfx::Bitmap> m_front;
+ Gfx::IntPoint m_old_position;
+ Type m_type;
+ uint8_t m_value;
+ bool m_old_position_valid { false };
+ bool m_moving { false };
+ bool m_upside_down { false };
+};
diff --git a/Userland/Games/Solitaire/CardStack.cpp b/Userland/Games/Solitaire/CardStack.cpp
new file mode 100644
index 0000000000..bb2b1bade5
--- /dev/null
+++ b/Userland/Games/Solitaire/CardStack.cpp
@@ -0,0 +1,238 @@
+/*
+ * Copyright (c) 2020, Till Mayer <till.mayer@web.de>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "CardStack.h"
+
+CardStack::CardStack()
+ : m_position({ 0, 0 })
+ , m_type(Invalid)
+ , m_base(m_position, { Card::width, Card::height })
+{
+}
+
+CardStack::CardStack(const Gfx::IntPoint& position, Type type)
+ : m_position(position)
+ , m_type(type)
+ , m_rules(rules_for_type(type))
+ , m_base(m_position, { Card::width, Card::height })
+{
+ ASSERT(type != Invalid);
+ calculate_bounding_box();
+}
+
+void CardStack::clear()
+{
+ m_stack.clear();
+ m_stack_positions.clear();
+}
+
+void CardStack::draw(GUI::Painter& painter, const Gfx::Color& background_color)
+{
+ switch (m_type) {
+ case Stock:
+ if (is_empty()) {
+ painter.fill_rect(m_base.shrunken(Card::width / 4, Card::height / 4), background_color.lightened(1.5));
+ painter.fill_rect(m_base.shrunken(Card::width / 2, Card::height / 2), background_color);
+ painter.draw_rect(m_base, background_color.darkened(0.5));
+ }
+ break;
+ case Foundation:
+ if (is_empty() || (m_stack.size() == 1 && peek().is_moving())) {
+ painter.draw_rect(m_base, background_color.darkened(0.5));
+ for (int y = 0; y < (m_base.height() - 4) / 8; ++y) {
+ for (int x = 0; x < (m_base.width() - 4) / 5; ++x) {
+ painter.draw_rect({ 4 + m_base.x() + x * 5, 4 + m_base.y() + y * 8, 1, 1 }, background_color.darkened(0.5));
+ }
+ }
+ }
+ break;
+ case Waste:
+ if (is_empty() || (m_stack.size() == 1 && peek().is_moving()))
+ painter.draw_rect(m_base, background_color.darkened(0.5));
+ break;
+ case Normal:
+ painter.draw_rect(m_base, background_color.darkened(0.5));
+ break;
+ default:
+ ASSERT_NOT_REACHED();
+ }
+
+ if (is_empty())
+ return;
+
+ if (m_rules.shift_x == 0 && m_rules.shift_y == 0) {
+ auto& card = peek();
+ card.draw(painter);
+ return;
+ }
+
+ for (auto& card : m_stack) {
+ if (!card.is_moving())
+ card.clear_and_draw(painter, background_color);
+ }
+
+ m_dirty = false;
+}
+
+void CardStack::rebound_cards()
+{
+ ASSERT(m_stack_positions.size() == m_stack.size());
+
+ size_t card_index = 0;
+ for (auto& card : m_stack)
+ card.set_position(m_stack_positions.at(card_index++));
+}
+
+void CardStack::add_all_grabbed_cards(const Gfx::IntPoint& click_location, NonnullRefPtrVector<Card>& grabbed)
+{
+ ASSERT(grabbed.is_empty());
+
+ if (m_type != Normal) {
+ auto& top_card = peek();
+ if (top_card.rect().contains(click_location)) {
+ top_card.set_moving(true);
+ grabbed.append(top_card);
+ }
+ return;
+ }
+
+ RefPtr<Card> last_intersect;
+
+ for (auto& card : m_stack) {
+ if (card.rect().contains(click_location)) {
+ if (card.is_upside_down())
+ continue;
+
+ last_intersect = card;
+ } else if (!last_intersect.is_null()) {
+ if (grabbed.is_empty()) {
+ grabbed.append(*last_intersect);
+ last_intersect->set_moving(true);
+ }
+
+ if (card.is_upside_down()) {
+ grabbed.clear();
+ return;
+ }
+
+ card.set_moving(true);
+ grabbed.append(card);
+ }
+ }
+
+ if (grabbed.is_empty() && !last_intersect.is_null()) {
+ grabbed.append(*last_intersect);
+ last_intersect->set_moving(true);
+ }
+}
+
+bool CardStack::is_allowed_to_push(const Card& card) const
+{
+ if (m_type == Stock || m_type == Waste)
+ return false;
+
+ if (m_type == Normal && is_empty())
+ return card.value() == 12;
+
+ if (m_type == Foundation && is_empty())
+ return card.value() == 0;
+
+ if (!is_empty()) {
+ auto& top_card = peek();
+ if (top_card.is_upside_down())
+ return false;
+
+ if (m_type == Foundation) {
+ return top_card.type() == card.type() && m_stack.size() == card.value();
+ } else if (m_type == Normal) {
+ return top_card.color() != card.color() && top_card.value() == card.value() + 1;
+ }
+
+ ASSERT_NOT_REACHED();
+ }
+
+ return true;
+}
+
+void CardStack::push(NonnullRefPtr<Card> card)
+{
+ auto size = m_stack.size();
+ auto top_most_position = m_stack_positions.is_empty() ? m_position : m_stack_positions.last();
+
+ if (size && size % m_rules.step == 0) {
+ if (peek().is_upside_down())
+ top_most_position.move_by(m_rules.shift_x, m_rules.shift_y_upside_down);
+ else
+ top_most_position.move_by(m_rules.shift_x, m_rules.shift_y);
+ }
+
+ if (m_type == Stock)
+ card->set_upside_down(true);
+
+ card->set_position(top_most_position);
+
+ m_stack.append(card);
+ m_stack_positions.append(top_most_position);
+ calculate_bounding_box();
+}
+
+NonnullRefPtr<Card> CardStack::pop()
+{
+ auto card = m_stack.take_last();
+
+ calculate_bounding_box();
+ if (m_type == Stock)
+ card->set_upside_down(false);
+
+ m_stack_positions.take_last();
+ return card;
+}
+
+void CardStack::calculate_bounding_box()
+{
+ m_bounding_box = Gfx::IntRect(m_position, { Card::width, Card::height });
+
+ if (m_stack.is_empty())
+ return;
+
+ uint16_t width = 0;
+ uint16_t height = 0;
+ size_t card_position = 0;
+ for (auto& card : m_stack) {
+ if (card_position % m_rules.step == 0 && card_position) {
+ if (card.is_upside_down()) {
+ width += m_rules.shift_x;
+ height += m_rules.shift_y_upside_down;
+ } else {
+ width += m_rules.shift_x;
+ height += m_rules.shift_y;
+ }
+ }
+ ++card_position;
+ }
+
+ m_bounding_box.set_size(Card::width + width, Card::height + height);
+}
diff --git a/Userland/Games/Solitaire/CardStack.h b/Userland/Games/Solitaire/CardStack.h
new file mode 100644
index 0000000000..18bac7a027
--- /dev/null
+++ b/Userland/Games/Solitaire/CardStack.h
@@ -0,0 +1,100 @@
+/*
+ * Copyright (c) 2020, Till Mayer <till.mayer@web.de>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include "Card.h"
+#include <AK/Vector.h>
+
+class CardStack final {
+public:
+ enum Type {
+ Invalid,
+ Stock,
+ Normal,
+ Waste,
+ Foundation
+ };
+
+ CardStack();
+ CardStack(const Gfx::IntPoint& position, Type type);
+
+ bool is_dirty() const { return m_dirty; }
+ bool is_empty() const { return m_stack.is_empty(); }
+ bool is_focused() const { return m_focused; }
+ Type type() const { return m_type; }
+ size_t count() const { return m_stack.size(); }
+ const Card& peek() const { return m_stack.last(); }
+ Card& peek() { return m_stack.last(); }
+ const Gfx::IntRect& bounding_box() const { return m_bounding_box; }
+
+ void set_focused(bool focused) { m_focused = focused; }
+ void set_dirty() { m_dirty = true; };
+
+ void push(NonnullRefPtr<Card> card);
+ NonnullRefPtr<Card> pop();
+ void rebound_cards();
+
+ bool is_allowed_to_push(const Card&) const;
+ void add_all_grabbed_cards(const Gfx::IntPoint& click_location, NonnullRefPtrVector<Card>& grabbed);
+ void draw(GUI::Painter&, const Gfx::Color& background_color);
+ void clear();
+
+private:
+ struct StackRules {
+ uint8_t shift_x { 0 };
+ uint8_t shift_y { 0 };
+ uint8_t step { 1 };
+ uint8_t shift_y_upside_down { 0 };
+ };
+
+ constexpr StackRules rules_for_type(Type stack_type)
+ {
+ switch (stack_type) {
+ case Foundation:
+ return { 2, 1, 4, 1 };
+ case Normal:
+ return { 0, 15, 1, 3 };
+ case Stock:
+ case Waste:
+ return { 2, 1, 8, 1 };
+ default:
+ return {};
+ }
+ }
+
+ void calculate_bounding_box();
+
+ NonnullRefPtrVector<Card> m_stack;
+ Vector<Gfx::IntPoint> m_stack_positions;
+ Gfx::IntPoint m_position;
+ Gfx::IntRect m_bounding_box;
+ Type m_type { Invalid };
+ StackRules m_rules;
+ bool m_focused { false };
+ bool m_dirty { false };
+ Gfx::IntRect m_base;
+};
diff --git a/Userland/Games/Solitaire/SolitaireWidget.cpp b/Userland/Games/Solitaire/SolitaireWidget.cpp
new file mode 100644
index 0000000000..d6497db677
--- /dev/null
+++ b/Userland/Games/Solitaire/SolitaireWidget.cpp
@@ -0,0 +1,446 @@
+/*
+ * Copyright (c) 2020, Till Mayer <till.mayer@web.de>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "SolitaireWidget.h"
+#include <LibCore/Timer.h>
+#include <LibGUI/Painter.h>
+#include <LibGUI/Window.h>
+#include <time.h>
+
+static const Color s_background_color { Color::from_rgb(0x008000) };
+static constexpr uint8_t new_game_animation_delay = 5;
+
+SolitaireWidget::SolitaireWidget(GUI::Window& window, Function<void(uint32_t)>&& on_score_update)
+ : m_on_score_update(move(on_score_update))
+{
+ set_fill_with_background_color(false);
+
+ m_stacks[Stock] = CardStack({ 10, 10 }, CardStack::Type::Stock);
+ m_stacks[Waste] = CardStack({ 10 + Card::width + 10, 10 }, CardStack::Type::Waste);
+ m_stacks[Foundation4] = CardStack({ SolitaireWidget::width - Card::width - 10, 10 }, CardStack::Type::Foundation);
+ m_stacks[Foundation3] = CardStack({ SolitaireWidget::width - 2 * Card::width - 20, 10 }, CardStack::Type::Foundation);
+ m_stacks[Foundation2] = CardStack({ SolitaireWidget::width - 3 * Card::width - 30, 10 }, CardStack::Type::Foundation);
+ m_stacks[Foundation1] = CardStack({ SolitaireWidget::width - 4 * Card::width - 40, 10 }, CardStack::Type::Foundation);
+ m_stacks[Pile1] = CardStack({ 10, 10 + Card::height + 10 }, CardStack::Type::Normal);
+ m_stacks[Pile2] = CardStack({ 10 + Card::width + 10, 10 + Card::height + 10 }, CardStack::Type::Normal);
+ m_stacks[Pile3] = CardStack({ 10 + 2 * Card::width + 20, 10 + Card::height + 10 }, CardStack::Type::Normal);
+ m_stacks[Pile4] = CardStack({ 10 + 3 * Card::width + 30, 10 + Card::height + 10 }, CardStack::Type::Normal);
+ m_stacks[Pile5] = CardStack({ 10 + 4 * Card::width + 40, 10 + Card::height + 10 }, CardStack::Type::Normal);
+ m_stacks[Pile6] = CardStack({ 10 + 5 * Card::width + 50, 10 + Card::height + 10 }, CardStack::Type::Normal);
+ m_stacks[Pile7] = CardStack({ 10 + 6 * Card::width + 60, 10 + Card::height + 10 }, CardStack::Type::Normal);
+
+ m_timer = Core::Timer::construct(1000 / 60, [&]() { tick(window); });
+ m_timer->stop();
+}
+
+SolitaireWidget::~SolitaireWidget()
+{
+}
+
+static float rand_float()
+{
+ return rand() / static_cast<float>(RAND_MAX);
+}
+
+void SolitaireWidget::tick(GUI::Window& window)
+{
+ if (!is_visible() || !updates_enabled() || !window.is_visible_for_timer_purposes())
+ return;
+
+ if (m_game_over_animation) {
+ ASSERT(!m_animation.card().is_null());
+ if (m_animation.card()->position().x() > SolitaireWidget::width || m_animation.card()->rect().right() < 0)
+ create_new_animation_card();
+
+ m_animation.tick();
+ }
+
+ if (m_has_to_repaint || m_game_over_animation || m_new_game_animation) {
+ m_repaint_all = false;
+ update();
+ }
+}
+
+void SolitaireWidget::create_new_animation_card()
+{
+ srand(time(nullptr));
+
+ auto card = Card::construct(static_cast<Card::Type>(rand() % Card::Type::__Count), rand() % Card::card_count);
+ card->set_position({ rand() % (SolitaireWidget::width - Card::width), rand() % (SolitaireWidget::height / 8) });
+
+ int x_sgn = card->position().x() > (SolitaireWidget::width / 2) ? -1 : 1;
+ m_animation = Animation(card, rand_float() + .4, x_sgn * ((rand() % 3) + 2), .6 + rand_float() * .4);
+}
+
+void SolitaireWidget::start_game_over_animation()
+{
+ if (m_game_over_animation)
+ return;
+
+ create_new_animation_card();
+ m_game_over_animation = true;
+}
+
+void SolitaireWidget::stop_game_over_animation()
+{
+ if (!m_game_over_animation)
+ return;
+
+ m_game_over_animation = false;
+ update();
+}
+
+void SolitaireWidget::setup()
+{
+ stop_game_over_animation();
+ m_timer->stop();
+
+ for (auto& stack : m_stacks)
+ stack.clear();
+
+ m_new_deck.clear();
+ m_new_game_animation_pile = 0;
+ m_score = 0;
+ update_score(0);
+
+ for (int i = 0; i < Card::card_count; ++i) {
+ m_new_deck.append(Card::construct(Card::Type::Clubs, i));
+ m_new_deck.append(Card::construct(Card::Type::Spades, i));
+ m_new_deck.append(Card::construct(Card::Type::Hearts, i));
+ m_new_deck.append(Card::construct(Card::Type::Diamonds, i));
+ }
+
+ srand(time(nullptr));
+ for (uint8_t i = 0; i < 200; ++i)
+ m_new_deck.append(m_new_deck.take(rand() % m_new_deck.size()));
+
+ m_new_game_animation = true;
+ m_timer->start();
+ update();
+}
+
+void SolitaireWidget::update_score(int to_add)
+{
+ m_score = max(static_cast<int>(m_score) + to_add, 0);
+ m_on_score_update(m_score);
+}
+
+void SolitaireWidget::keydown_event(GUI::KeyEvent& event)
+{
+ if (m_new_game_animation || m_game_over_animation)
+ return;
+
+ if (event.key() == KeyCode::Key_F12)
+ start_game_over_animation();
+}
+
+void SolitaireWidget::mousedown_event(GUI::MouseEvent& event)
+{
+ GUI::Widget::mousedown_event(event);
+
+ if (m_new_game_animation || m_game_over_animation)
+ return;
+
+ auto click_location = event.position();
+ for (auto& to_check : m_stacks) {
+ if (to_check.bounding_box().contains(click_location)) {
+ if (to_check.type() == CardStack::Type::Stock) {
+ auto& waste = stack(Waste);
+ auto& stock = stack(Stock);
+
+ if (stock.is_empty()) {
+ if (waste.is_empty())
+ return;
+
+ while (!waste.is_empty()) {
+ auto card = waste.pop();
+ stock.push(card);
+ }
+
+ stock.set_dirty();
+ waste.set_dirty();
+ m_has_to_repaint = true;
+ update_score(-100);
+ } else {
+ move_card(stock, waste);
+ }
+ } else if (!to_check.is_empty()) {
+ auto& top_card = to_check.peek();
+
+ if (top_card.is_upside_down()) {
+ if (top_card.rect().contains(click_location)) {
+ top_card.set_upside_down(false);
+ to_check.set_dirty();
+ update_score(5);
+ m_has_to_repaint = true;
+ }
+ } else if (m_focused_cards.is_empty()) {
+ to_check.add_all_grabbed_cards(click_location, m_focused_cards);
+ m_mouse_down_location = click_location;
+ to_check.set_focused(true);
+ m_focused_stack = &to_check;
+ m_mouse_down = true;
+ }
+ }
+ break;
+ }
+ }
+}
+
+void SolitaireWidget::mouseup_event(GUI::MouseEvent& event)
+{
+ GUI::Widget::mouseup_event(event);
+
+ if (!m_focused_stack || m_focused_cards.is_empty() || m_game_over_animation || m_new_game_animation)
+ return;
+
+ bool rebound = true;
+ for (auto& stack : m_stacks) {
+ if (stack.is_focused())
+ continue;
+
+ for (auto& focused_card : m_focused_cards) {
+ if (stack.bounding_box().intersects(focused_card.rect())) {
+ if (stack.is_allowed_to_push(m_focused_cards.at(0))) {
+ for (auto& to_intersect : m_focused_cards) {
+ mark_intersecting_stacks_dirty(to_intersect);
+ stack.push(to_intersect);
+ m_focused_stack->pop();
+ }
+
+ m_focused_stack->set_dirty();
+ stack.set_dirty();
+
+ if (m_focused_stack->type() == CardStack::Type::Waste && stack.type() == CardStack::Type::Normal) {
+ update_score(5);
+ } else if (m_focused_stack->type() == CardStack::Type::Waste && stack.type() == CardStack::Type::Foundation) {
+ update_score(10);
+ } else if (m_focused_stack->type() == CardStack::Type::Normal && stack.type() == CardStack::Type::Foundation) {
+ update_score(10);
+ } else if (m_focused_stack->type() == CardStack::Type::Foundation && stack.type() == CardStack::Type::Normal) {
+ update_score(-15);
+ }
+
+ rebound = false;
+ break;
+ }
+ }
+ }
+ }
+
+ if (rebound) {
+ for (auto& to_intersect : m_focused_cards)
+ mark_intersecting_stacks_dirty(to_intersect);
+
+ m_focused_stack->rebound_cards();
+ m_focused_stack->set_dirty();
+ }
+
+ m_mouse_down = false;
+ m_has_to_repaint = true;
+}
+
+void SolitaireWidget::mousemove_event(GUI::MouseEvent& event)
+{
+ GUI::Widget::mousemove_event(event);
+
+ if (!m_mouse_down || m_game_over_animation || m_new_game_animation)
+ return;
+
+ auto click_location = event.position();
+ int dx = click_location.dx_relative_to(m_mouse_down_location);
+ int dy = click_location.dy_relative_to(m_mouse_down_location);
+
+ for (auto& to_intersect : m_focused_cards) {
+ mark_intersecting_stacks_dirty(to_intersect);
+ to_intersect.rect().move_by(dx, dy);
+ }
+
+ m_mouse_down_location = click_location;
+ m_has_to_repaint = true;
+}
+
+void SolitaireWidget::doubleclick_event(GUI::MouseEvent& event)
+{
+ GUI::Widget::doubleclick_event(event);
+
+ if (m_game_over_animation) {
+ start_game_over_animation();
+ setup();
+ return;
+ }
+
+ if (m_new_game_animation)
+ return;
+
+ auto click_location = event.position();
+ for (auto& to_check : m_stacks) {
+ if (to_check.type() == CardStack::Type::Foundation || to_check.type() == CardStack::Type::Stock)
+ continue;
+
+ if (to_check.bounding_box().contains(click_location) && !to_check.is_empty()) {
+ auto& top_card = to_check.peek();
+ if (!top_card.is_upside_down() && top_card.rect().contains(click_location)) {
+ if (stack(Foundation1).is_allowed_to_push(top_card))
+ move_card(to_check, stack(Foundation1));
+ else if (stack(Foundation2).is_allowed_to_push(top_card))
+ move_card(to_check, stack(Foundation2));
+ else if (stack(Foundation3).is_allowed_to_push(top_card))
+ move_card(to_check, stack(Foundation3));
+ else if (stack(Foundation4).is_allowed_to_push(top_card))
+ move_card(to_check, stack(Foundation4));
+ else
+ break;
+
+ update_score(10);
+ }
+ break;
+ }
+ }
+
+ m_has_to_repaint = true;
+}
+
+void SolitaireWidget::check_for_game_over()
+{
+ for (auto& stack : m_stacks) {
+ if (stack.type() != CardStack::Type::Foundation)
+ continue;
+ if (stack.count() != Card::card_count)
+ return;
+ }
+
+ start_game_over_animation();
+}
+
+void SolitaireWidget::move_card(CardStack& from, CardStack& to)
+{
+ auto card = from.pop();
+
+ card->set_moving(true);
+ m_focused_cards.clear();
+ m_focused_cards.append(card);
+ mark_intersecting_stacks_dirty(card);
+ to.push(card);
+
+ from.set_dirty();
+ to.set_dirty();
+
+ m_has_to_repaint = true;
+}
+
+void SolitaireWidget::mark_intersecting_stacks_dirty(Card& intersecting_card)
+{
+ for (auto& stack : m_stacks) {
+ if (intersecting_card.rect().intersects(stack.bounding_box())) {
+ stack.set_dirty();
+ m_has_to_repaint = true;
+ }
+ }
+}
+
+void SolitaireWidget::paint_event(GUI::PaintEvent& event)
+{
+ GUI::Widget::paint_event(event);
+
+ m_has_to_repaint = false;
+ if (m_game_over_animation && m_repaint_all)
+ return;
+
+ GUI::Painter painter(*this);
+
+ if (m_repaint_all) {
+ /* Only start the timer when update() got called from the
+ window manager, or else we might end up with a blank screen */
+ if (!m_timer->is_active())
+ m_timer->start();
+
+ painter.fill_rect(event.rect(), s_background_color);
+
+ for (auto& stack : m_stacks)
+ stack.draw(painter, s_background_color);
+ } else if (m_game_over_animation && !m_animation.card().is_null()) {
+ m_animation.card()->draw(painter);
+ } else if (m_new_game_animation) {
+ if (m_new_game_animation_delay < new_game_animation_delay) {
+ ++m_new_game_animation_delay;
+ } else {
+ m_new_game_animation_delay = 0;
+ auto& current_pile = stack(piles.at(m_new_game_animation_pile));
+
+ if (current_pile.count() < m_new_game_animation_pile) {
+ auto card = m_new_deck.take_last();
+ card->set_upside_down(true);
+ current_pile.push(card);
+ } else {
+ current_pile.push(m_new_deck.take_last());
+ ++m_new_game_animation_pile;
+ }
+ current_pile.set_dirty();
+
+ if (m_new_game_animation_pile == piles.size()) {
+ while (!m_new_deck.is_empty())
+ stack(Stock).push(m_new_deck.take_last());
+ stack(Stock).set_dirty();
+ m_new_game_animation = false;
+ }
+ }
+ }
+
+ if (!m_game_over_animation && !m_repaint_all) {
+ if (!m_focused_cards.is_empty()) {
+ for (auto& focused_card : m_focused_cards)
+ focused_card.clear(painter, s_background_color);
+ }
+
+ for (auto& stack : m_stacks) {
+ if (stack.is_dirty())
+ stack.draw(painter, s_background_color);
+ }
+
+ if (!m_focused_cards.is_empty()) {
+ for (auto& focused_card : m_focused_cards) {
+ focused_card.draw(painter);
+ focused_card.save_old_position();
+ }
+ }
+ }
+
+ m_repaint_all = true;
+ if (!m_mouse_down) {
+ if (!m_focused_cards.is_empty()) {
+ check_for_game_over();
+ for (auto& card : m_focused_cards)
+ card.set_moving(false);
+ m_focused_cards.clear();
+ }
+
+ if (m_focused_stack) {
+ m_focused_stack->set_focused(false);
+ m_focused_stack = nullptr;
+ }
+ }
+}
diff --git a/Userland/Games/Solitaire/SolitaireWidget.h b/Userland/Games/Solitaire/SolitaireWidget.h
new file mode 100644
index 0000000000..213251715d
--- /dev/null
+++ b/Userland/Games/Solitaire/SolitaireWidget.h
@@ -0,0 +1,142 @@
+/*
+ * Copyright (c) 2020, Till Mayer <till.mayer@web.de>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include "CardStack.h"
+#include <LibGUI/Painter.h>
+#include <LibGUI/Widget.h>
+
+class SolitaireWidget final : public GUI::Widget {
+ C_OBJECT(SolitaireWidget)
+public:
+ static constexpr int width = 640;
+ static constexpr int height = 480;
+
+ virtual ~SolitaireWidget() override;
+ void setup();
+
+private:
+ SolitaireWidget(GUI::Window&, Function<void(uint32_t)>&& on_score_update);
+
+ class Animation {
+ public:
+ Animation()
+ {
+ }
+
+ Animation(RefPtr<Card> animation_card, float gravity, int x_vel, float bouncyness)
+ : m_animation_card(animation_card)
+ , m_gravity(gravity)
+ , m_x_velocity(x_vel)
+ , m_bouncyness(bouncyness)
+ {
+ }
+
+ RefPtr<Card> card() { return m_animation_card; }
+
+ void tick()
+ {
+ ASSERT(!m_animation_card.is_null());
+ m_y_velocity += m_gravity;
+
+ if (m_animation_card->position().y() + Card::height + m_y_velocity > SolitaireWidget::height + 1 && m_y_velocity > 0) {
+ m_y_velocity = min((m_y_velocity * -m_bouncyness), -8.f);
+ m_animation_card->rect().set_y(SolitaireWidget::height - Card::height);
+ m_animation_card->rect().move_by(m_x_velocity, 0);
+ } else {
+ m_animation_card->rect().move_by(m_x_velocity, m_y_velocity);
+ }
+ }
+
+ private:
+ RefPtr<Card> m_animation_card;
+ float m_gravity { 0 };
+ int m_x_velocity { 0 };
+ float m_y_velocity { 0 };
+ float m_bouncyness { 0 };
+ };
+
+ enum StackLocation {
+ Stock,
+ Waste,
+ Foundation1,
+ Foundation2,
+ Foundation3,
+ Foundation4,
+ Pile1,
+ Pile2,
+ Pile3,
+ Pile4,
+ Pile5,
+ Pile6,
+ Pile7,
+ __Count
+ };
+ static constexpr Array piles = { Pile1, Pile2, Pile3, Pile4, Pile5, Pile6, Pile7 };
+
+ void mark_intersecting_stacks_dirty(Card& intersecting_card);
+ void update_score(int to_add);
+ void move_card(CardStack& from, CardStack& to);
+ void start_game_over_animation();
+ void stop_game_over_animation();
+ void create_new_animation_card();
+ void check_for_game_over();
+ void tick(GUI::Window&);
+
+ ALWAYS_INLINE CardStack& stack(StackLocation location)
+ {
+ return m_stacks[location];
+ }
+
+ virtual void paint_event(GUI::PaintEvent&) override;
+ virtual void mousedown_event(GUI::MouseEvent&) override;
+ virtual void mouseup_event(GUI::MouseEvent&) override;
+ virtual void mousemove_event(GUI::MouseEvent&) override;
+ virtual void doubleclick_event(GUI::MouseEvent&) override;
+ virtual void keydown_event(GUI::KeyEvent&) override;
+
+ RefPtr<Core::Timer> m_timer;
+ NonnullRefPtrVector<Card> m_focused_cards;
+ NonnullRefPtrVector<Card> m_new_deck;
+ CardStack m_stacks[StackLocation::__Count];
+ CardStack* m_focused_stack { nullptr };
+ Gfx::IntPoint m_mouse_down_location;
+
+ bool m_mouse_down { false };
+ bool m_repaint_all { true };
+ bool m_has_to_repaint { true };
+
+ Animation m_animation;
+ bool m_game_over_animation { false };
+
+ bool m_new_game_animation { false };
+ uint8_t m_new_game_animation_pile { 0 };
+ uint8_t m_new_game_animation_delay { 0 };
+
+ uint32_t m_score { 0 };
+ Function<void(uint32_t)> m_on_score_update;
+};
diff --git a/Userland/Games/Solitaire/main.cpp b/Userland/Games/Solitaire/main.cpp
new file mode 100644
index 0000000000..de04b18218
--- /dev/null
+++ b/Userland/Games/Solitaire/main.cpp
@@ -0,0 +1,85 @@
+/*
+ * Copyright (c) 2020, Till Mayer <till.mayer@web.de>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "SolitaireWidget.h"
+#include <LibGUI/Action.h>
+#include <LibGUI/Application.h>
+#include <LibGUI/Icon.h>
+#include <LibGUI/Menu.h>
+#include <LibGUI/MenuBar.h>
+#include <LibGUI/Window.h>
+#include <stdio.h>
+#include <unistd.h>
+
+int main(int argc, char** argv)
+{
+ auto app = GUI::Application::construct(argc, argv);
+ auto app_icon = GUI::Icon::default_icon("app-solitaire");
+
+ if (pledge("stdio rpath shared_buffer", nullptr) < 0) {
+ perror("pledge");
+ return 1;
+ }
+
+ if (unveil("/res", "r") < 0) {
+ perror("unveil");
+ return 1;
+ }
+
+ if (unveil(nullptr, nullptr) < 0) {
+ perror("unveil");
+ return 1;
+ }
+
+ auto window = GUI::Window::construct();
+
+ window->set_resizable(false);
+ window->resize(SolitaireWidget::width, SolitaireWidget::height);
+
+ auto widget = SolitaireWidget::construct(window, [&](uint32_t score) {
+ window->set_title(String::formatted("Score: {} - Solitaire", score));
+ });
+
+ auto menubar = GUI::MenuBar::construct();
+ auto& app_menu = menubar->add_menu("Solitaire");
+
+ app_menu.add_action(GUI::Action::create("New game", { Mod_None, Key_F2 }, [&](auto&) {
+ widget->setup();
+ }));
+ app_menu.add_separator();
+ app_menu.add_action(GUI::CommonActions::make_quit_action([&](auto&) { app->quit(); }));
+
+ auto& help_menu = menubar->add_menu("Help");
+ help_menu.add_action(GUI::CommonActions::make_about_action("Solitaire", app_icon, window));
+
+ app->set_menubar(move(menubar));
+ window->set_main_widget(widget);
+ window->set_icon(app_icon.bitmap_for_size(16));
+ window->show();
+ widget->setup();
+
+ return app->exec();
+}