From 5619ed1dc63b87c01b48e2134183b829ba490f1d Mon Sep 17 00:00:00 2001 From: AnotherTest Date: Wed, 19 Aug 2020 20:47:17 +0430 Subject: [PATCH] 2048: Make board size and target tile configurable This adds a "settings" option that allows the user to configure the board size and target tile, and optionally save them to a config file. Closes #3219. --- Games/2048/BoardView.cpp | 19 ++++++- Games/2048/BoardView.h | 1 + Games/2048/CMakeLists.txt | 1 + Games/2048/Game.cpp | 26 ++++++++-- Games/2048/Game.h | 13 ++++- Games/2048/GameSizeDialog.cpp | 95 +++++++++++++++++++++++++++++++++++ Games/2048/GameSizeDialog.h | 45 +++++++++++++++++ Games/2048/main.cpp | 74 +++++++++++++++++++++++---- 8 files changed, 259 insertions(+), 15 deletions(-) create mode 100644 Games/2048/GameSizeDialog.cpp create mode 100644 Games/2048/GameSizeDialog.h diff --git a/Games/2048/BoardView.cpp b/Games/2048/BoardView.cpp index 7978bd5a16c..b04a84973e8 100644 --- a/Games/2048/BoardView.cpp +++ b/Games/2048/BoardView.cpp @@ -44,7 +44,18 @@ 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(); } @@ -89,6 +100,11 @@ size_t BoardView::columns() const } void BoardView::resize_event(GUI::ResizeEvent&) +{ + resize(); +} + +void BoardView::resize() { constexpr float padding_ratio = 7; m_padding = min( @@ -154,7 +170,8 @@ Gfx::Color BoardView::background_color_for_cell(u32 value) case 2048: return Color::from_rgb(0xedc22e); default: - ASSERT_NOT_REACHED(); + ASSERT(value > 2048); + return Color::from_rgb(0x3c3a32); } } diff --git a/Games/2048/BoardView.h b/Games/2048/BoardView.h index 87b28da00ab..d5a0679d1a4 100644 --- a/Games/2048/BoardView.h +++ b/Games/2048/BoardView.h @@ -49,6 +49,7 @@ private: size_t columns() const; void pick_font(); + void resize(); Color background_color_for_cell(u32 value); Color text_color_for_cell(u32 value); diff --git a/Games/2048/CMakeLists.txt b/Games/2048/CMakeLists.txt index d239331a447..2b92309c76e 100644 --- a/Games/2048/CMakeLists.txt +++ b/Games/2048/CMakeLists.txt @@ -1,6 +1,7 @@ set(SOURCES BoardView.cpp Game.cpp + GameSizeDialog.cpp main.cpp ) diff --git a/Games/2048/Game.cpp b/Games/2048/Game.cpp index cf029426bb8..6b3bd7b2bbb 100644 --- a/Games/2048/Game.cpp +++ b/Games/2048/Game.cpp @@ -25,11 +25,19 @@ */ #include "Game.h" +#include #include -Game::Game(size_t grid_size) +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); @@ -132,10 +140,10 @@ static Game::Board slide_left(const Game::Board& board, size_t& successful_merge return new_board; } -static bool is_complete(const Game::Board& board) +static bool is_complete(const Game::Board& board, size_t target) { for (auto& row : board) { - if (row.contains_slow(2048)) + if (row.contains_slow(target)) return true; } @@ -201,7 +209,7 @@ Game::MoveOutcome Game::attempt_move(Direction direction) m_score += successful_merge_score; } - if (is_complete(m_board)) + if (is_complete(m_board, m_target_tile)) return MoveOutcome::Won; if (is_stalled(m_board)) return MoveOutcome::GameOver; @@ -209,3 +217,13 @@ Game::MoveOutcome Game::attempt_move(Direction direction) 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/Games/2048/Game.h b/Games/2048/Game.h index 031364f7017..570dae671ca 100644 --- a/Games/2048/Game.h +++ b/Games/2048/Game.h @@ -30,7 +30,7 @@ class Game final { public: - Game(size_t); + Game(size_t board_size, size_t target_tile = 0); Game(const Game&) = default; enum class MoveOutcome { @@ -51,15 +51,26 @@ public: 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>; 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 }; diff --git a/Games/2048/GameSizeDialog.cpp b/Games/2048/GameSizeDialog.cpp new file mode 100644 index 00000000000..a9ef824351c --- /dev/null +++ b/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 +#include +#include +#include +#include + +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(); + main_widget.set_fill_with_background_color(true); + + auto& layout = main_widget.set_layout(); + layout.set_margins({ 4, 4, 4, 4 }); + + auto& board_size_box = main_widget.add(); + auto& input_layout = board_size_box.set_layout(); + input_layout.set_spacing(4); + + board_size_box.add("Board size").set_text_alignment(Gfx::TextAlignment::CenterLeft); + auto& spinbox = board_size_box.add(); + + auto& target_box = main_widget.add(); + auto& target_layout = target_box.set_layout(); + target_layout.set_spacing(4); + spinbox.set_min(2); + spinbox.set_value(m_board_size); + + target_box.add("Target tile").set_text_alignment(Gfx::TextAlignment::CenterLeft); + auto& tile_value_label = target_box.add(String::number(target_tile())); + tile_value_label.set_text_alignment(Gfx::TextAlignment::CenterRight); + auto& target_spinbox = target_box.add(); + 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("Temporary"); + temp_checkbox.set_checked(m_temporary); + temp_checkbox.on_checked = [this](auto checked) { m_temporary = checked; }; + + auto& buttonbox = main_widget.add(); + auto& button_layout = buttonbox.set_layout(); + button_layout.set_spacing(10); + + buttonbox.add("Cancel").on_click = [this](auto) { + done(Dialog::ExecCancel); + }; + + buttonbox.add("OK").on_click = [this](auto) { + done(Dialog::ExecOK); + }; +} diff --git a/Games/2048/GameSizeDialog.h b/Games/2048/GameSizeDialog.h new file mode 100644 index 00000000000..ba6122bc81c --- /dev/null +++ b/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 +#include + +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/Games/2048/main.cpp b/Games/2048/main.cpp index 22d0dd82be2..969a61d60ac 100644 --- a/Games/2048/main.cpp +++ b/Games/2048/main.cpp @@ -26,6 +26,8 @@ #include "BoardView.h" #include "Game.h" +#include "GameSizeDialog.h" +#include #include #include #include @@ -52,7 +54,17 @@ int main(int argc, char** argv) auto window = GUI::Window::construct(); - if (pledge("stdio rpath shared_buffer wpath accept", nullptr) < 0) { + 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; } @@ -62,6 +74,11 @@ int main(int argc, char** argv) return 1; } + if (unveil(config->file_name().characters(), "crw") < 0) { + perror("unveil"); + return 1; + } + if (unveil(nullptr, nullptr) < 0) { perror("unveil"); return 1; @@ -75,7 +92,7 @@ int main(int argc, char** argv) main_widget.set_layout(); main_widget.set_fill_with_background_color(true); - Game game { 4 }; + Game game { board_size, target_tile }; auto& board_view = main_widget.add(&game.board()); board_view.set_focus(true); @@ -89,13 +106,45 @@ int main(int argc, char** argv) update(); - auto start_a_new_game = [&]() { - game = Game(4); - update(); - }; - Vector 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); @@ -111,7 +160,7 @@ int main(int argc, char** argv) case Game::MoveOutcome::Won: update(); GUI::MessageBox::show(window, - String::format("Score = %d in %zu turns", game.score(), game.turns()), + String::format("You reached %d in %zu turns with a score of %d", game.target_tile(), game.turns(), game.score()), "You won!", GUI::MessageBox::Type::Information); start_a_new_game(); @@ -119,7 +168,7 @@ int main(int argc, char** argv) case Game::MoveOutcome::GameOver: update(); GUI::MessageBox::show(window, - String::format("Score = %d in %zu turns", game.score(), game.turns()), + String::format("You reached %d in %zu turns with a score of %d", game.largest_tile(), game.turns(), game.score()), "You lost!", GUI::MessageBox::Type::Information); start_a_new_game(); @@ -134,13 +183,20 @@ int main(int argc, char** argv) 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(); }));