diff options
Diffstat (limited to 'Userland/Games')
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(); +} |