From 7eca8f7e6221ab626ca1e7803e42ca4dec6e9bcb Mon Sep 17 00:00:00 2001 From: implicitfield <114500360+implicitfield@users.noreply.github.com> Date: Thu, 13 Oct 2022 22:01:31 +0300 Subject: [PATCH] Games: Add Flood --- Base/res/apps/Flood.af | 4 + Base/res/icons/16x16/app-flood.png | Bin 0 -> 119 bytes Base/res/icons/32x32/app-flood.png | Bin 0 -> 116 bytes Base/usr/share/man/man6/Flood.md | 21 ++ Userland/Games/CMakeLists.txt | 1 + Userland/Games/Flood/Board.cpp | 131 ++++++++++++ Userland/Games/Flood/Board.h | 56 +++++ Userland/Games/Flood/BoardWidget.cpp | 95 +++++++++ Userland/Games/Flood/BoardWidget.h | 41 ++++ Userland/Games/Flood/CMakeLists.txt | 23 +++ Userland/Games/Flood/FloodWindow.gml | 17 ++ Userland/Games/Flood/SettingsDialog.cpp | 76 +++++++ Userland/Games/Flood/SettingsDialog.gml | 71 +++++++ Userland/Games/Flood/SettingsDialog.h | 25 +++ Userland/Games/Flood/main.cpp | 261 ++++++++++++++++++++++++ 15 files changed, 822 insertions(+) create mode 100644 Base/res/apps/Flood.af create mode 100644 Base/res/icons/16x16/app-flood.png create mode 100644 Base/res/icons/32x32/app-flood.png create mode 100644 Base/usr/share/man/man6/Flood.md create mode 100644 Userland/Games/Flood/Board.cpp create mode 100644 Userland/Games/Flood/Board.h create mode 100644 Userland/Games/Flood/BoardWidget.cpp create mode 100644 Userland/Games/Flood/BoardWidget.h create mode 100644 Userland/Games/Flood/CMakeLists.txt create mode 100644 Userland/Games/Flood/FloodWindow.gml create mode 100644 Userland/Games/Flood/SettingsDialog.cpp create mode 100644 Userland/Games/Flood/SettingsDialog.gml create mode 100644 Userland/Games/Flood/SettingsDialog.h create mode 100644 Userland/Games/Flood/main.cpp diff --git a/Base/res/apps/Flood.af b/Base/res/apps/Flood.af new file mode 100644 index 0000000000..45dfc9c01c --- /dev/null +++ b/Base/res/apps/Flood.af @@ -0,0 +1,4 @@ +[App] +Name=Flood +Executable=/bin/Flood +Category=Games diff --git a/Base/res/icons/16x16/app-flood.png b/Base/res/icons/16x16/app-flood.png new file mode 100644 index 0000000000000000000000000000000000000000..abb746898da2f2a0663391552998147d48d60193 GIT binary patch literal 119 zcmeAS@N?(olHy`uVBq!ia0y~yU=RRd7G?$phPQVgfdu#id_r6q82&RfFxcDM|8H>k z|DQqhsJ|iu1B0@si(?4K_2hsiCN@?!Ha2l_adjifQ^$@SYieq8N-;_}$H2fICYWXZ Tugj5vfq}u()z4*}Q$iB}bQ&Kk literal 0 HcmV?d00001 diff --git a/Base/res/icons/32x32/app-flood.png b/Base/res/icons/32x32/app-flood.png new file mode 100644 index 0000000000000000000000000000000000000000..34455290d1f7d2a70548fa4922c31926439532f9 GIT binary patch literal 116 zcmeAS@N?(olHy`uVBq!ia0y~yU{C;I7G?$phQ^Te;|vT8`~f~8t_%$S85$Vu?d|_J zIQ;+5AbQka5v0!3#W6(Vd~$#jGtUer9)Sl;jE7?nNHDP|ChXuA=&EIqIOW1P`Ju`r Q1_lNOPgg&ebxsLQ0MJMsApigX literal 0 HcmV?d00001 diff --git a/Base/usr/share/man/man6/Flood.md b/Base/usr/share/man/man6/Flood.md new file mode 100644 index 0000000000..cb74f38a8c --- /dev/null +++ b/Base/usr/share/man/man6/Flood.md @@ -0,0 +1,21 @@ +## Name + +![Icon](/res/icons/16x16/app-flood.png) Flood + +[Open](file:///bin/Flood) + +## Synopsis + +```**sh +$ Flood +``` + +## Description + +Flood is a game where the goal is to fill the entire board with cells of the same color in the most efficient way possible. + +The flooding begins from the top-left corner and continues in whichever direction chosen by selecting any cell of the wanted color. + +## Settings + +The size of the board and the color scheme can be changed in the settings. diff --git a/Userland/Games/CMakeLists.txt b/Userland/Games/CMakeLists.txt index 7732fd5f80..e04e3f28a0 100644 --- a/Userland/Games/CMakeLists.txt +++ b/Userland/Games/CMakeLists.txt @@ -1,6 +1,7 @@ add_subdirectory(2048) add_subdirectory(Chess) add_subdirectory(FlappyBug) +add_subdirectory(Flood) add_subdirectory(GameOfLife) add_subdirectory(Hearts) add_subdirectory(MasterWord) diff --git a/Userland/Games/Flood/Board.cpp b/Userland/Games/Flood/Board.cpp new file mode 100644 index 0000000000..a1be59535c --- /dev/null +++ b/Userland/Games/Flood/Board.cpp @@ -0,0 +1,131 @@ +/* + * Copyright (c) 2022, the SerenityOS developers. + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include "Board.h" +#include +#include + +Board::Board(size_t rows, size_t columns) +{ + resize(rows, columns); +} + +void Board::clear() +{ + for (size_t row = 0; row < m_rows; ++row) { + for (size_t column = 0; column < m_columns; ++column) { + set_cell(row, column, Color::Transparent); + } + } +} + +bool Board::is_flooded() const +{ + auto first_cell_color = cell(0, 0).release_value(); + for (size_t row = 0; row < rows(); ++row) { + for (size_t column = 0; column < columns(); ++column) { + if (first_cell_color == cell(row, column).release_value()) + continue; + return false; + } + } + return true; +} + +void Board::randomize() +{ + for (size_t row = 0; row < m_rows; ++row) { + for (size_t column = 0; column < m_columns; ++column) { + auto const& color = m_colors[get_random_uniform(m_colors.size())]; + set_cell(row, column, color); + } + } + set_current_color(cell(0, 0).release_value()); +} + +void Board::resize(size_t rows, size_t columns) +{ + m_rows = rows; + m_columns = columns; + + // Vector values get default-initialized, we don't need to set them explicitly. + m_cells.resize(rows); + for (size_t row = 0; row < rows; ++row) + m_cells[row].resize(columns); +} + +void Board::set_cell(size_t row, size_t column, Color color) +{ + VERIFY(row < m_rows && column < m_columns); + + m_cells[row][column] = color; +} + +ErrorOr Board::cell(size_t row, size_t column) const +{ + if (row >= m_rows || column >= m_columns) + return Error::from_string_literal("No such cell."); + + return m_cells[row][column]; +} + +void Board::set_current_color(Color new_color) +{ + m_previous_color = m_current_color; + m_current_color = new_color; +} + +void Board::set_color_scheme(Vector colors) +{ + VERIFY(colors.size() == 8); + m_colors = move(colors); +} + +void Board::reset() +{ + clear(); + set_current_color(Color::Transparent); + m_previous_color = Color::Transparent; +} + +// Adapted from Userland/PixelPaint/Tools/BucketTool.cpp::flood_fill. +u32 Board::update_colors(bool only_calculate_flooded_area) +{ + Queue points_to_visit; + + points_to_visit.enqueue({ 0, 0 }); + set_cell(0, 0, get_current_color()); + + Vector> visited_board; + visited_board.resize(cells().size()); + for (size_t row = 0; row < cells().size(); ++row) + visited_board[row].resize(cells()[row].size()); + u32 painted = 1; + + // This implements a non-recursive flood fill. This is a breadth-first search of paintable neighbors + // As we find neighbors that are paintable we update their pixel, add them to the queue, and mark them in the "visited_board". + while (!points_to_visit.is_empty()) { + auto current_point = points_to_visit.dequeue(); + auto candidate_points = Array { + current_point.moved_left(1), + current_point.moved_right(1), + current_point.moved_up(1), + current_point.moved_down(1) + }; + for (auto candidate_point : candidate_points) { + if (cell(candidate_point.y(), candidate_point.x()).is_error()) + continue; + if (!visited_board[candidate_point.y()][candidate_point.x()] && cell(candidate_point.y(), candidate_point.x()).release_value() == (only_calculate_flooded_area ? get_current_color() : get_previous_color())) { + ++painted; + points_to_visit.enqueue(candidate_point); + visited_board[candidate_point.y()][candidate_point.x()] = true; + if (!only_calculate_flooded_area) + set_cell(candidate_point.y(), candidate_point.x(), get_current_color()); + } + } + } + return painted; +} diff --git a/Userland/Games/Flood/Board.h b/Userland/Games/Flood/Board.h new file mode 100644 index 0000000000..18f4b65f6e --- /dev/null +++ b/Userland/Games/Flood/Board.h @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2022, the SerenityOS developers. + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include +#include +#include +#include +#include +#include + +class Board { +public: + Board(size_t rows, size_t columns); + ~Board() = default; + + size_t columns() const { return m_columns; } + size_t rows() const { return m_rows; } + + bool is_flooded() const; + void set_cell(size_t row, size_t column, Color color); + ErrorOr cell(size_t row, size_t column) const; + auto const& cells() const { return m_cells; } + + void clear(); + void randomize(); + void reset(); + void resize(size_t rows, size_t columns); + u32 update_colors(bool only_calculate_flooded_area = false); + + Color get_current_color() { return m_current_color; } + Color get_previous_color() { return m_previous_color; } + Vector get_color_scheme() { return m_colors; } + + void set_current_color(Color new_color); + void set_color_scheme(Vector colors); + + struct RowAndColumn { + size_t row { 0 }; + size_t column { 0 }; + }; + +private: + size_t m_rows { 0 }; + size_t m_columns { 0 }; + + Color m_current_color; + Color m_previous_color; + + Vector m_colors; + Vector> m_cells; +}; diff --git a/Userland/Games/Flood/BoardWidget.cpp b/Userland/Games/Flood/BoardWidget.cpp new file mode 100644 index 0000000000..f43e7d335b --- /dev/null +++ b/Userland/Games/Flood/BoardWidget.cpp @@ -0,0 +1,95 @@ +/* + * Copyright (c) 2022, the SerenityOS developers. + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include "BoardWidget.h" + +#include +#include + +BoardWidget::BoardWidget(size_t rows, size_t columns, Vector colors, Color background_color) + : m_board(make(rows, columns)) + , m_background_color(background_color) +{ + m_board->set_color_scheme(move(colors)); +} + +void BoardWidget::resize_board(size_t rows, size_t columns) +{ + if (columns == m_board->columns() && rows == m_board->rows()) + return; + m_board->resize(rows, columns); +} + +void BoardWidget::set_background_color(Color const background) +{ + m_background_color = background; +} + +int BoardWidget::get_cell_size() const +{ + int width = rect().width() / m_board->columns(); + int height = rect().height() / m_board->rows(); + + return min(width, height); +} + +Gfx::IntSize BoardWidget::get_board_offset() const +{ + int cell_size = get_cell_size(); + return { + (width() - cell_size * m_board->columns()) / 2, + (height() - cell_size * m_board->rows()) / 2, + }; +} + +void BoardWidget::paint_event(GUI::PaintEvent& event) +{ + GUI::Widget::paint_event(event); + + GUI::Painter painter(*this); + painter.add_clip_rect(event.rect()); + painter.fill_rect(event.rect(), m_background_color); + + int cell_size = get_cell_size(); + Gfx::IntSize board_offset = get_board_offset(); + + for (size_t row = 0; row < m_board->rows(); ++row) { + for (size_t column = 0; column < m_board->columns(); ++column) { + int cell_x = column * cell_size + board_offset.width(); + int cell_y = row * cell_size + board_offset.height(); + + Gfx::Rect cell_rect(cell_x, cell_y, cell_size, cell_size); + Color fill_color = m_board->cell(row, column).release_value(); + painter.fill_rect(cell_rect, fill_color); + } + } +} + +void BoardWidget::mousedown_event(GUI::MouseEvent& event) +{ + if (event.button() == GUI::MouseButton::Primary && on_move) { + auto row_and_column = get_row_and_column_for_point(event.x(), event.y()); + if (!row_and_column.has_value()) + return; + on_move(row_and_column.value()); + } +} + +Optional BoardWidget::get_row_and_column_for_point(int x, int y) const +{ + auto board_offset = get_board_offset(); + auto cell_size = get_cell_size(); + auto board_width = m_board->columns() * cell_size; + auto board_height = m_board->rows() * cell_size; + if (x <= board_offset.width() || static_cast(x) >= board_offset.width() + board_width) + return {}; + if (y <= board_offset.height() || static_cast(y) >= board_offset.height() + board_height) + return {}; + return { { + .row = static_cast((y - board_offset.height()) / cell_size), + .column = static_cast((x - board_offset.width()) / cell_size), + } }; +} diff --git a/Userland/Games/Flood/BoardWidget.h b/Userland/Games/Flood/BoardWidget.h new file mode 100644 index 0000000000..e584252483 --- /dev/null +++ b/Userland/Games/Flood/BoardWidget.h @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2022, the SerenityOS developers. + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include "Board.h" +#include +#include +#include +#include +#include +#include + +class BoardWidget final : public GUI::Widget { + C_OBJECT(BoardWidget); + +public: + Function on_move; + + virtual void paint_event(GUI::PaintEvent&) override; + virtual void mousedown_event(GUI::MouseEvent&) override; + + void set_background_color(Color const background); + + int get_cell_size() const; + Gfx::IntSize get_board_offset() const; + + Optional get_row_and_column_for_point(int x, int y) const; + + void resize_board(size_t rows, size_t columns); + Board* board() { return m_board.ptr(); } + +private: + BoardWidget(size_t rows, size_t columns, Vector colors, Color background_color); + NonnullOwnPtr m_board; + + Color m_background_color; +}; diff --git a/Userland/Games/Flood/CMakeLists.txt b/Userland/Games/Flood/CMakeLists.txt new file mode 100644 index 0000000000..5faeed61fb --- /dev/null +++ b/Userland/Games/Flood/CMakeLists.txt @@ -0,0 +1,23 @@ +serenity_component( + Flood + RECOMMENDED + TARGETS Flood +) + +compile_gml(FloodWindow.gml FloodWindowGML.h flood_window_gml) +compile_gml(SettingsDialog.gml SettingsDialogGML.h settings_dialog_gml) + +set(SOURCES + Board.cpp + BoardWidget.cpp + SettingsDialog.cpp + main.cpp +) + +set(GENERATED_SOURCES + FloodWindowGML.h + SettingsDialogGML.h +) + +serenity_app(Flood ICON app-flood) +target_link_libraries(Flood LibConfig LibGUI LibMain LibDesktop) diff --git a/Userland/Games/Flood/FloodWindow.gml b/Userland/Games/Flood/FloodWindow.gml new file mode 100644 index 0000000000..17703a0abb --- /dev/null +++ b/Userland/Games/Flood/FloodWindow.gml @@ -0,0 +1,17 @@ +@GUI::Frame { + fill_with_background_color: true + layout: @GUI::VerticalBoxLayout {} + + @GUI::Widget { + layout: @GUI::VerticalBoxLayout {} + + @GUI::Widget { + name: "board_widget_container" + layout: @GUI::VerticalBoxLayout {} + } + + @GUI::Statusbar { + name: "statusbar" + } + } +} diff --git a/Userland/Games/Flood/SettingsDialog.cpp b/Userland/Games/Flood/SettingsDialog.cpp new file mode 100644 index 0000000000..1d7a97bf29 --- /dev/null +++ b/Userland/Games/Flood/SettingsDialog.cpp @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2022, the SerenityOS developers. + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include "SettingsDialog.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +SettingsDialog::SettingsDialog(GUI::Window* parent, size_t board_rows, size_t board_columns, StringView color_scheme) + : GUI::Dialog(parent) + , m_board_rows(board_rows) + , m_board_columns(board_columns) + , m_color_scheme(color_scheme) +{ + set_rect({ 0, 0, 250, 150 }); + set_title("New Game"); + set_icon(parent->icon()); + set_resizable(false); + + auto& main_widget = set_main_widget(); + if (!main_widget.load_from_gml(settings_dialog_gml)) + VERIFY_NOT_REACHED(); + + auto board_rows_spinbox = main_widget.find_descendant_of_type_named("board_rows_spinbox"); + board_rows_spinbox->set_value(m_board_rows); + + board_rows_spinbox->on_change = [&](auto value) { + m_board_rows = value; + }; + + auto board_columns_spinbox = main_widget.find_descendant_of_type_named("board_columns_spinbox"); + board_columns_spinbox->set_value(m_board_columns); + + board_columns_spinbox->on_change = [&](auto value) { + m_board_columns = value; + }; + + static Vector color_scheme_names; + color_scheme_names.clear(); + Core::DirIterator iterator("/res/terminal-colors", Core::DirIterator::SkipParentAndBaseDir); + while (iterator.has_next()) { + auto path = iterator.next_path(); + color_scheme_names.append(path.replace(".ini"sv, ""sv, ReplaceMode::FirstOnly)); + } + quick_sort(color_scheme_names); + + auto color_scheme_combo = main_widget.find_descendant_of_type_named("color_scheme_combo"); + color_scheme_combo->set_only_allow_values_from_model(true); + color_scheme_combo->set_model(*GUI::ItemListModel::create(color_scheme_names)); + color_scheme_combo->set_selected_index(color_scheme_names.find_first_index(m_color_scheme).value()); + color_scheme_combo->set_enabled(color_scheme_names.size() > 1); + color_scheme_combo->on_change = [&](auto&, const GUI::ModelIndex& index) { + m_color_scheme = index.data().as_string(); + }; + + auto cancel_button = main_widget.find_descendant_of_type_named("cancel_button"); + cancel_button->on_click = [this](auto) { + done(ExecResult::Cancel); + }; + + auto ok_button = main_widget.find_descendant_of_type_named("ok_button"); + ok_button->on_click = [this](auto) { + done(ExecResult::OK); + }; +} diff --git a/Userland/Games/Flood/SettingsDialog.gml b/Userland/Games/Flood/SettingsDialog.gml new file mode 100644 index 0000000000..84c3804df1 --- /dev/null +++ b/Userland/Games/Flood/SettingsDialog.gml @@ -0,0 +1,71 @@ +@GUI::Frame { + fill_with_background_color: true + layout: @GUI::VerticalBoxLayout { + margins: [4] + } + + @GUI::Widget { + layout: @GUI::HorizontalBoxLayout { + spacing: 4 + } + + @GUI::Label { + text: "Board rows" + text_alignment: "CenterLeft" + } + + @GUI::SpinBox { + name: "board_rows_spinbox" + max: 32 + min: 2 + } + } + + @GUI::Widget { + layout: @GUI::HorizontalBoxLayout { + spacing: 4 + } + + @GUI::Label { + text: "Board columns" + text_alignment: "CenterLeft" + } + + @GUI::SpinBox { + name: "board_columns_spinbox" + max: 32 + min: 2 + } + } + + @GUI::Widget { + layout: @GUI::HorizontalBoxLayout { + spacing: 4 + } + + @GUI::Label { + text: "Color scheme" + text_alignment: "CenterLeft" + } + + @GUI::ComboBox { + name: "color_scheme_combo" + } + } + + @GUI::Widget { + layout: @GUI::HorizontalBoxLayout { + spacing: 10 + } + + @GUI::Button { + name: "cancel_button" + text: "Cancel" + } + + @GUI::Button { + name: "ok_button" + text: "OK" + } + } +} diff --git a/Userland/Games/Flood/SettingsDialog.h b/Userland/Games/Flood/SettingsDialog.h new file mode 100644 index 0000000000..4b40c8223e --- /dev/null +++ b/Userland/Games/Flood/SettingsDialog.h @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2022, the SerenityOS developers. + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include +#include + +class SettingsDialog : public GUI::Dialog { + C_OBJECT(SettingsDialog) +public: + size_t board_rows() const { return m_board_rows; } + size_t board_columns() const { return m_board_columns; } + StringView color_scheme() const { return m_color_scheme; } + +private: + SettingsDialog(GUI::Window* parent, size_t board_rows, size_t board_columns, StringView color_scheme); + + size_t m_board_rows; + size_t m_board_columns; + String m_color_scheme; +}; diff --git a/Userland/Games/Flood/main.cpp b/Userland/Games/Flood/main.cpp new file mode 100644 index 0000000000..d400cdf0ea --- /dev/null +++ b/Userland/Games/Flood/main.cpp @@ -0,0 +1,261 @@ +/* + * Copyright (c) 2022, the SerenityOS developers. + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include "BoardWidget.h" +#include "SettingsDialog.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// FIXME: Move this into a library. Also consider simplifying obtaining 'color_scheme_names' in +// SettingsDialog.cpp and Userland/Applications/TerminalSettings/TerminalSettingsWidget.cpp. +// Adapted from Libraries/LibVT/TerminalWidget.cpp::TerminalWidget::set_color_scheme. +static ErrorOr> get_color_scheme_from_string(StringView name) +{ + if (name.contains('/')) { + return Error::from_string_literal("Shenanigans! Color scheme names can't contain slashes."); + } + + constexpr StringView color_names[] = { + "Black"sv, + "Red"sv, + "Green"sv, + "Yellow"sv, + "Blue"sv, + "Magenta"sv, + "Cyan"sv, + "White"sv + }; + + auto const path = String::formatted("/res/terminal-colors/{}.ini", name); + auto color_config_or_error = Core::ConfigFile::open(path); + if (color_config_or_error.is_error()) { + return Error::from_string_view(String::formatted("Unable to read color scheme file '{}': {}", path, color_config_or_error.error())); + } + auto const color_config = color_config_or_error.release_value(); + Vector colors; + + for (u8 color_index = 0; color_index < 8; ++color_index) { + auto const rgb = Gfx::Color::from_string(color_config->read_entry("Bright", color_names[color_index])); + if (rgb.has_value()) + colors.append(Color::from_argb(rgb.value().value())); + } + + auto const default_background = Gfx::Color::from_string(color_config->read_entry("Primary", "Background")); + if (default_background.has_value()) + colors.append(default_background.value()); + else + colors.append(Color::DarkGray); + + return colors; +} + +// FIXME: Improve this AI. +// Currently, this AI always chooses a move that gets the most cells flooded immidiately. +// This far from being able to generate an optimal solution, and is something that needs to be improved +// if a user-facing auto-solver is implemented or a harder difficulty is wanted. +// A fairly simple way to improve this would be to test deeper moves and then choose the most efficient sequence. +static int get_number_of_moves_from_ai(Board const& board) +{ + Board optimal_board { board }; + auto const color_scheme = optimal_board.get_color_scheme(); + optimal_board.set_current_color(optimal_board.cell(0, 0).release_value()); + int moves { 0 }; + while (!optimal_board.is_flooded()) { + ++moves; + int most_painted = 0; + Color optimal_color = optimal_board.cell(0, 0).release_value(); + for (size_t i = 0; i < color_scheme.size(); ++i) { + Board test_board { optimal_board }; + test_board.set_current_color(color_scheme[i]); + // The first update applies the current color, and the second update is done to obtain the new area. + test_board.update_colors(); + int new_area = test_board.update_colors(true); + if (new_area > most_painted) { + most_painted = new_area; + optimal_color = color_scheme[i]; + } + } + optimal_board.set_current_color(optimal_color); + optimal_board.update_colors(); + } + return moves; +} + +ErrorOr serenity_main(Main::Arguments arguments) +{ + TRY(Core::System::pledge("stdio rpath recvfd sendfd unix")); + auto app = TRY(GUI::Application::try_create(arguments)); + auto app_icon = TRY(GUI::Icon::try_create_default_icon("app-flood"sv)); + + auto window = TRY(GUI::Window::try_create()); + + Config::pledge_domain("Flood"); + + TRY(Desktop::Launcher::add_allowed_handler_with_only_specific_urls("/bin/Help", { URL::create_with_file_scheme("/usr/share/man/man6/Flood.md") })); + TRY(Desktop::Launcher::seal_allowlist()); + + TRY(Core::System::pledge("stdio rpath recvfd sendfd")); + + TRY(Core::System::unveil("/tmp/session/%sid/portal/launch", "rw")); + TRY(Core::System::unveil("/res", "r")); + TRY(Core::System::unveil(nullptr, nullptr)); + + size_t board_rows = Config::read_i32("Flood"sv, ""sv, "board_rows"sv, 16); + size_t board_columns = Config::read_i32("Flood"sv, ""sv, "board_columns"sv, 16); + String color_scheme = Config::read_string("Flood"sv, ""sv, "color_scheme"sv, "Default"sv); + + Config::write_i32("Flood"sv, ""sv, "board_rows"sv, board_rows); + Config::write_i32("Flood"sv, ""sv, "board_columns"sv, board_columns); + Config::write_string("Flood"sv, ""sv, "color_scheme"sv, color_scheme); + + window->set_double_buffering_enabled(false); + window->set_title("Flood"); + window->resize(304, 325); + + auto& main_widget = window->set_main_widget(); + if (!main_widget.load_from_gml(flood_window_gml)) + VERIFY_NOT_REACHED(); + + auto colors_or_error { get_color_scheme_from_string(color_scheme) }; + if (colors_or_error.is_error()) + return colors_or_error.release_error(); + auto colors = colors_or_error.release_value(); + auto background_color = colors.take_last(); + + auto board_widget = TRY(main_widget.find_descendant_of_type_named("board_widget_container")->try_add(board_rows, board_columns, move(colors), move(background_color))); + board_widget->board()->randomize(); + int ai_moves = get_number_of_moves_from_ai(*board_widget->board()); + int moves_made = 0; + + auto statusbar = main_widget.find_descendant_of_type_named("statusbar"); + + app->on_action_enter = [&](GUI::Action& action) { + auto text = action.status_tip(); + if (text.is_empty()) + text = Gfx::parse_ampersand_string(action.text()); + statusbar->set_override_text(move(text)); + }; + + app->on_action_leave = [&](GUI::Action&) { + statusbar->set_override_text({}); + }; + + auto update = [&]() { + board_widget->update(); + statusbar->set_text(String::formatted("Moves remaining: {}", ai_moves - moves_made)); + }; + + update(); + + auto change_settings = [&] { + auto settings_dialog = SettingsDialog::construct(window, board_rows, board_columns, color_scheme); + if (settings_dialog->exec() != GUI::Dialog::ExecResult::OK) + return; + + board_rows = settings_dialog->board_rows(); + board_columns = settings_dialog->board_columns(); + color_scheme = settings_dialog->color_scheme(); + + Config::write_i32("Flood"sv, ""sv, "board_rows"sv, board_rows); + Config::write_i32("Flood"sv, ""sv, "board_columns"sv, board_columns); + Config::write_string("Flood"sv, ""sv, "color_scheme"sv, color_scheme); + + GUI::MessageBox::show(settings_dialog, "New settings have been saved and will be applied on a new game"sv, "Settings Changed Successfully"sv, GUI::MessageBox::Type::Information); + }; + + auto start_a_new_game = [&] { + board_widget->resize_board(board_rows, board_columns); + board_widget->board()->reset(); + auto colors_or_error = get_color_scheme_from_string(color_scheme); + if (!colors_or_error.is_error()) { + auto colors = colors_or_error.release_value(); + board_widget->set_background_color(colors.take_last()); + board_widget->board()->set_color_scheme(move(colors)); + board_widget->board()->randomize(); + ai_moves = get_number_of_moves_from_ai(*board_widget->board()); + moves_made = 0; + } else { + GUI::MessageBox::show(window, "The chosen color scheme could not be set"sv, "Choose another one and try again"sv, GUI::MessageBox::Type::Error); + } + update(); + window->update(); + }; + + board_widget->on_move = [&](Board::RowAndColumn row_and_column) { + auto const [row, column] = row_and_column; + board_widget->board()->set_current_color(board_widget->board()->cell(row, column).release_value()); + if (board_widget->board()->get_previous_color() != board_widget->board()->get_current_color()) { + ++moves_made; + board_widget->board()->update_colors(); + update(); + if (board_widget->board()->is_flooded()) { + String dialog_text("You have tied with the AI."sv); + auto dialog_title("Congratulations!"sv); + if (ai_moves - moves_made == 1) + dialog_text = "You defeated the AI by 1 move."sv; + else if (ai_moves - moves_made > 1) + dialog_text = String::formatted("You defeated the AI by {} moves.", ai_moves - moves_made); + else + dialog_title = "Game over!"sv; + GUI::MessageBox::show(window, + dialog_text, + dialog_title, + GUI::MessageBox::Type::Information, + GUI::MessageBox::InputType::OK); + start_a_new_game(); + } else if (moves_made == ai_moves) { + GUI::MessageBox::show(window, + StringView("You have no more moves left."sv), + "You lost!"sv, + GUI::MessageBox::Type::Information, + GUI::MessageBox::InputType::OK); + start_a_new_game(); + } + } + }; + + auto game_menu = TRY(window->try_add_menu("&Game")); + + TRY(game_menu->try_add_action(GUI::Action::create("&New Game", { Mod_None, Key_F2 }, TRY(Gfx::Bitmap::try_load_from_file("/res/icons/16x16/reload.png"sv)), [&](auto&) { + start_a_new_game(); + }))); + + TRY(game_menu->try_add_separator()); + TRY(game_menu->try_add_action(GUI::Action::create("&Settings", TRY(Gfx::Bitmap::try_load_from_file("/res/icons/16x16/settings.png"sv)), [&](auto&) { + change_settings(); + }))); + + TRY(game_menu->try_add_separator()); + TRY(game_menu->try_add_action(GUI::CommonActions::make_quit_action([](auto&) { + GUI::Application::the()->quit(); + }))); + + auto help_menu = TRY(window->try_add_menu("&Help")); + TRY(help_menu->try_add_action(GUI::CommonActions::make_command_palette_action(window))); + TRY(help_menu->try_add_action(GUI::CommonActions::make_help_action([](auto&) { + Desktop::Launcher::open(URL::create_with_file_scheme("/usr/share/man/man6/Flood.md"), "/bin/Help"); + }))); + TRY(help_menu->try_add_action(GUI::CommonActions::make_about_action("Flood", app_icon, window))); + + window->show(); + + window->set_icon(app_icon.bitmap_for_size(16)); + + return app->exec(); +}