diff options
-rw-r--r-- | Base/res/apps/Conway.af | 4 | ||||
-rw-r--r-- | Base/res/apps/GameOfLife.af | 4 | ||||
-rw-r--r-- | Base/res/icons/16x16/app-conway.png | bin | 197 -> 0 bytes | |||
-rw-r--r-- | Base/res/icons/16x16/app-gameoflife.png | bin | 0 -> 115 bytes | |||
-rw-r--r-- | Base/res/icons/32x32/app-conway.png | bin | 275 -> 0 bytes | |||
-rw-r--r-- | Base/res/icons/32x32/app-gameoflife.png | bin | 0 -> 139 bytes | |||
-rw-r--r-- | Userland/Games/CMakeLists.txt | 2 | ||||
-rw-r--r-- | Userland/Games/Conway/CMakeLists.txt | 7 | ||||
-rw-r--r-- | Userland/Games/Conway/Game.cpp | 163 | ||||
-rw-r--r-- | Userland/Games/Conway/Game.h | 41 | ||||
-rw-r--r-- | Userland/Games/Conway/main.cpp | 72 | ||||
-rw-r--r-- | Userland/Games/GameOfLife/Board.cpp | 132 | ||||
-rw-r--r-- | Userland/Games/GameOfLife/Board.h | 50 | ||||
-rw-r--r-- | Userland/Games/GameOfLife/BoardWidget.cpp | 172 | ||||
-rw-r--r-- | Userland/Games/GameOfLife/BoardWidget.h | 70 | ||||
-rw-r--r-- | Userland/Games/GameOfLife/CMakeLists.txt | 11 | ||||
-rw-r--r-- | Userland/Games/GameOfLife/GameOfLife.gml | 57 | ||||
-rw-r--r-- | Userland/Games/GameOfLife/main.cpp | 170 |
18 files changed, 667 insertions, 288 deletions
diff --git a/Base/res/apps/Conway.af b/Base/res/apps/Conway.af deleted file mode 100644 index a158d53362..0000000000 --- a/Base/res/apps/Conway.af +++ /dev/null @@ -1,4 +0,0 @@ -[App] -Name=Conway -Executable=/bin/Conway -Category=Games diff --git a/Base/res/apps/GameOfLife.af b/Base/res/apps/GameOfLife.af new file mode 100644 index 0000000000..b23f9dca47 --- /dev/null +++ b/Base/res/apps/GameOfLife.af @@ -0,0 +1,4 @@ +[App] +Name=Game Of Life +Executable=/bin/GameOfLife +Category=Games diff --git a/Base/res/icons/16x16/app-conway.png b/Base/res/icons/16x16/app-conway.png Binary files differdeleted file mode 100644 index ca39bc1866..0000000000 --- a/Base/res/icons/16x16/app-conway.png +++ /dev/null diff --git a/Base/res/icons/16x16/app-gameoflife.png b/Base/res/icons/16x16/app-gameoflife.png Binary files differnew file mode 100644 index 0000000000..158a774090 --- /dev/null +++ b/Base/res/icons/16x16/app-gameoflife.png diff --git a/Base/res/icons/32x32/app-conway.png b/Base/res/icons/32x32/app-conway.png Binary files differdeleted file mode 100644 index af831a8996..0000000000 --- a/Base/res/icons/32x32/app-conway.png +++ /dev/null diff --git a/Base/res/icons/32x32/app-gameoflife.png b/Base/res/icons/32x32/app-gameoflife.png Binary files differnew file mode 100644 index 0000000000..9360efa7bb --- /dev/null +++ b/Base/res/icons/32x32/app-gameoflife.png diff --git a/Userland/Games/CMakeLists.txt b/Userland/Games/CMakeLists.txt index ea879ed24c..fabe700499 100644 --- a/Userland/Games/CMakeLists.txt +++ b/Userland/Games/CMakeLists.txt @@ -1,7 +1,7 @@ add_subdirectory(2048) add_subdirectory(Breakout) add_subdirectory(Chess) -add_subdirectory(Conway) +add_subdirectory(GameOfLife) add_subdirectory(Minesweeper) add_subdirectory(Pong) add_subdirectory(Snake) diff --git a/Userland/Games/Conway/CMakeLists.txt b/Userland/Games/Conway/CMakeLists.txt deleted file mode 100644 index 18312c363a..0000000000 --- a/Userland/Games/Conway/CMakeLists.txt +++ /dev/null @@ -1,7 +0,0 @@ -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 deleted file mode 100644 index 054bbde4e6..0000000000 --- a/Userland/Games/Conway/Game.cpp +++ /dev/null @@ -1,163 +0,0 @@ -/* - * Copyright (c) 2021, the SerenityOS developers. - * - * SPDX-License-Identifier: BSD-2-Clause - */ - -#include "Game.h" -#include <AK/Random.h> -#include <LibGUI/Painter.h> -#include <stdlib.h> -#include <time.h> - -Game::Game() -{ - 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] = (get_random<u32>() % 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(); -} - -Gfx::IntRect Game::first_cell_rect() const -{ - 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; - return { x_margin, y_margin, cell_size.width(), cell_size.height() }; -} - -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 first_rect = first_cell_rect(); - - for (int y = 0; y < m_rows; y++) { - for (int x = 0; x < m_columns; x++) { - Gfx::IntRect rect { - x * first_rect.width() + first_rect.left(), - y * first_rect.height() + first_rect.top(), - first_rect.width(), - first_rect.height() - }; - painter.fill_rect(rect, m_universe[y][x] ? m_alive_color : m_dead_color); - } - } -} - -void Game::mousedown_event(GUI::MouseEvent& event) -{ - switch (event.button()) { - case GUI::MouseButton::Left: - case GUI::MouseButton::Right: - m_last_button = event.button(); - break; - default: - return; - } - interact_at(event.position()); -} - -void Game::mouseup_event(GUI::MouseEvent& event) -{ - if (event.button() == m_last_button) - m_last_button = GUI::MouseButton::None; -} - -void Game::mousemove_event(GUI::MouseEvent& event) -{ - interact_at(event.position()); -} - -void Game::interact_at(const Gfx::IntPoint& point) -{ - if (m_last_button == GUI::MouseButton::None) - return; - - auto first_rect = first_cell_rect(); - // Too tiny window, we don't actually display anything. - if (first_rect.width() == 0 || first_rect.height() == 0) - return; - - // Too far left/up. - if (point.x() < first_rect.left() || point.y() < first_rect.top()) - return; - - int cell_x = (point.x() - first_rect.left()) / first_rect.width(); - int cell_y = (point.y() - first_rect.top()) / first_rect.height(); - - // Too far right/down. - if (cell_x >= m_columns || cell_y >= m_rows) - return; - - switch (m_last_button) { - case GUI::MouseButton::Left: - m_universe[cell_y][cell_x] = true; - break; - case GUI::MouseButton::Right: - m_universe[cell_y][cell_x] = false; - break; - default: - VERIFY_NOT_REACHED(); - } -} diff --git a/Userland/Games/Conway/Game.h b/Userland/Games/Conway/Game.h deleted file mode 100644 index 634f92cbf7..0000000000 --- a/Userland/Games/Conway/Game.h +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright (c) 2021, the SerenityOS developers. - * - * SPDX-License-Identifier: BSD-2-Clause - */ - -#pragma once - -#include <LibGUI/Widget.h> - -class Game : public GUI::Widget { - C_OBJECT(Game) -public: - virtual ~Game() override; - void reset(); - - int rows() const { return m_rows; }; - int columns() const { return m_columns; }; - -private: - Game(); - virtual void paint_event(GUI::PaintEvent&) override; - virtual void timer_event(Core::TimerEvent&) override; - virtual void mousedown_event(GUI::MouseEvent&) override; - virtual void mouseup_event(GUI::MouseEvent&) override; - virtual void mousemove_event(GUI::MouseEvent&) override; - - Gfx::IntRect first_cell_rect() const; - void seed_universe(); - void update_universe(); - void interact_at(const Gfx::IntPoint&); - - const Gfx::Color m_alive_color { Color::Green }; - const Gfx::Color m_dead_color { Color::Black }; - const int m_rows { 200 }; - const int m_columns { 200 }; - const int m_sleep { 100 }; - GUI::MouseButton m_last_button { GUI::MouseButton::None }; - - bool m_universe[200][200]; -}; diff --git a/Userland/Games/Conway/main.cpp b/Userland/Games/Conway/main.cpp deleted file mode 100644 index dc4325dfc7..0000000000 --- a/Userland/Games/Conway/main.cpp +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Copyright (c) 2021, the SerenityOS developers. - * - * SPDX-License-Identifier: BSD-2-Clause - */ - -#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> -#include <unistd.h> - -int main(int argc, char** argv) -{ - if (pledge("stdio rpath wpath cpath recvfd sendfd cpath unix", nullptr) < 0) { - perror("pledge"); - return 1; - } - - auto app = GUI::Application::construct(argc, argv); - - if (pledge("stdio rpath recvfd sendfd", 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>(); - window->set_minimum_size(game.columns(), game.rows()); - - auto menubar = GUI::Menubar::construct(); - - auto& game_menu = menubar->add_menu("&Game"); - - game_menu.add_action(GUI::Action::create("&Reset", { Mod_None, Key_F2 }, [&](auto&) { - game.reset(); - })); - game_menu.add_separator(); - game_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)); - - window->set_menubar(move(menubar)); - - window->show(); - - return app->exec(); -} diff --git a/Userland/Games/GameOfLife/Board.cpp b/Userland/Games/GameOfLife/Board.cpp new file mode 100644 index 0000000000..452c803776 --- /dev/null +++ b/Userland/Games/GameOfLife/Board.cpp @@ -0,0 +1,132 @@ +/* + * Copyright (c) 2021, Andres Crucitti <dasc495@gmail.com> + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include "Board.h" +#include <AK/Random.h> +#include <time.h> + +Board::Board(size_t rows, size_t columns) + : m_columns(columns) + , m_rows(rows) +{ + m_cells.resize(total_size()); + for (size_t i = 0; i < total_size(); ++i) { + m_cells[i] = false; + } +} + +Board::~Board() +{ +} + +void Board::run_generation() +{ + m_stalled = true; + Vector<bool> new_cells; + new_cells.resize(total_size()); + + for (size_t i = 0; i < total_size(); ++i) { + bool old_val = m_cells[i]; + new_cells[i] = calculate_next_value(i); + if (old_val != new_cells[i]) { + m_stalled = false; + } + } + + if (m_stalled) + return; + + m_cells = new_cells; +} + +bool Board::calculate_next_value(size_t index) const +{ + size_t row = index / columns(); + size_t column = index % columns(); + + int top_left = cell(row - 1, column - 1); + int top_mid = cell(row - 1, column); + int top_right = cell(row - 1, column + 1); + int left = cell(row, column - 1); + int right = cell(row, column + 1); + int bottom_left = cell(row + 1, column - 1); + int bottom_mid = cell(row + 1, column); + int bottom_right = cell(row + 1, column + 1); + + int sum = top_left + top_mid + top_right + left + right + bottom_left + bottom_mid + bottom_right; + + bool current = m_cells[index]; + bool new_value = current; + + if (current) { + if (sum < 2 || sum > 3) + new_value = false; + } else { + if (sum == 3) + new_value = true; + } + + return new_value; +} + +void Board::clear() +{ + for (size_t i = 0; i < total_size(); ++i) + set_cell(i, false); +} + +void Board::randomize() +{ + for (size_t i = 0; i < total_size(); ++i) + set_cell(i, get_random<u32>() % 2); +} + +void Board::toggle_cell(size_t index) +{ + VERIFY(index < total_size()); + + m_cells[index] = !m_cells[index]; +} + +void Board::toggle_cell(size_t row, size_t column) +{ + VERIFY(column < total_size() && row < total_size()); + + size_t index = calculate_index(row, column); + set_cell(index, !m_cells[index]); +} + +void Board::set_cell(size_t index, bool on) +{ + VERIFY(index < total_size()); + + m_cells[index] = on; +} + +void Board::set_cell(size_t row, size_t column, bool on) +{ + VERIFY(column < total_size() && row < total_size()); + + size_t index = calculate_index(row, column); + set_cell(index, on); +} + +bool Board::cell(size_t index) const +{ + if (index > total_size() - 1) + return false; + + return m_cells[index]; +} + +bool Board::cell(size_t row, size_t column) const +{ + if (column > total_size() - 1 || row > total_size() - 1) + return false; + + size_t index = calculate_index(row, column); + return cell(index); +} diff --git a/Userland/Games/GameOfLife/Board.h b/Userland/Games/GameOfLife/Board.h new file mode 100644 index 0000000000..86fe40c807 --- /dev/null +++ b/Userland/Games/GameOfLife/Board.h @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2021, Andres Crucitti <dasc495@gmail.com> + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include <AK/Vector.h> +#include <LibGfx/Point.h> +#include <stdio.h> + +class Board { +public: + Board(size_t rows, size_t column); + ~Board(); + + size_t total_size() const { return m_columns * m_rows; } + size_t columns() const { return m_columns; } + size_t rows() const { return m_rows; } + + size_t calculate_index(size_t row, size_t column) const { return row * m_columns + column; }; + + void toggle_cell(size_t index); + void toggle_cell(size_t row, size_t column); + + void set_cell(size_t row, size_t column, bool on); + void set_cell(size_t index, bool on); + + bool cell(size_t row, size_t column) const; + bool cell(size_t index) const; + + const Vector<bool>& cells() const { return m_cells; } + + void run_generation(); + bool is_stalled() const { return m_stalled; } + + void clear(); + void randomize(); + +private: + bool calculate_next_value(size_t index) const; + + size_t m_columns { 1 }; + size_t m_rows { 1 }; + + bool m_stalled { false }; + + Vector<bool> m_cells; +}; diff --git a/Userland/Games/GameOfLife/BoardWidget.cpp b/Userland/Games/GameOfLife/BoardWidget.cpp new file mode 100644 index 0000000000..eb7051bcd4 --- /dev/null +++ b/Userland/Games/GameOfLife/BoardWidget.cpp @@ -0,0 +1,172 @@ +/* + * Copyright (c) 2021, Andres Crucitti <dasc495@gmail.com> + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include "BoardWidget.h" +#include <LibGUI/Painter.h> + +BoardWidget::BoardWidget(size_t rows, size_t columns) +{ + m_timer = add<Core::Timer>(); + m_timer->stop(); + m_timer->on_timeout = [this] { + run_generation(); + }; + m_timer->set_interval(m_running_timer_interval); + + update_board(rows, columns); +} + +void BoardWidget::run_generation() +{ + m_board->run_generation(); + update(); + if (m_board->is_stalled()) { + if (on_stall) + on_stall(); + update(); + }; +} + +void BoardWidget::update_board(size_t rows, size_t columns) +{ + set_running(false); + + m_last_cell_toggled = columns * rows; + + if (m_board) { + if (columns == m_board->columns() && rows == m_board->rows()) { + return; + } + } + + m_board = make<Board>(rows, columns); +} + +void BoardWidget::set_running_timer_interval(int interval) +{ + if (is_running()) + return; + + m_running_timer_interval = interval; + m_timer->set_interval(m_running_timer_interval); + + if (on_running_state_change) + on_running_state_change(); +} + +void BoardWidget::set_running(bool running) +{ + if (running == m_running) + return; + + m_running = running; + + if (m_running) { + m_timer->start(); + } else { + m_timer->stop(); + } + + if (on_running_state_change) + on_running_state_change(); + + update(); +} + +void BoardWidget::toggle_cell(size_t index) +{ + if (m_running || !m_toggling_cells || m_last_cell_toggled == index) + return; + + m_last_cell_toggled = index; + m_board->toggle_cell(index); + + if (on_cell_toggled) + on_cell_toggled(m_board, index); + + update(); +} + +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(), Color::Black); + + 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 border_color = Color::DarkGray; + Color fill_color; + + bool on = m_board->cell(row, column); + if (on) { + fill_color = Color::from_rgb(Gfx::make_rgb(220, 220, 80)); + } else { + fill_color = Color::MidGray; + } + + painter.fill_rect(cell_rect, fill_color); + if (cell_size > 4) { + painter.draw_rect(cell_rect, border_color); + } + } + } +} + +void BoardWidget::mousedown_event(GUI::MouseEvent& event) +{ + size_t index = get_index_for_point(event.x(), event.y()); + set_toggling_cells(true); + toggle_cell(index); +} + +void BoardWidget::mousemove_event(GUI::MouseEvent& event) +{ + size_t index = get_index_for_point(event.x(), event.y()); + if (is_toggling()) { + if (last_toggled() != index) + toggle_cell(index); + } +} + +void BoardWidget::mouseup_event(GUI::MouseEvent&) +{ + set_toggling_cells(false); +} + +size_t BoardWidget::get_index_for_point(int x, int y) const +{ + int cell_size = get_cell_size(); + Gfx::IntSize board_offset = get_board_offset(); + return m_board->columns() * ((y - board_offset.height()) / cell_size) + (x - board_offset.width()) / cell_size; +} diff --git a/Userland/Games/GameOfLife/BoardWidget.h b/Userland/Games/GameOfLife/BoardWidget.h new file mode 100644 index 0000000000..0d10e2daa2 --- /dev/null +++ b/Userland/Games/GameOfLife/BoardWidget.h @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2021, Andres Crucitti <dasc495@gmail.com> + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include "Board.h" +#include <LibCore/Timer.h> +#include <LibGUI/Widget.h> + +class BoardWidget final : public GUI::Widget { + C_OBJECT(BoardWidget); + +public: + virtual void paint_event(GUI::PaintEvent&) override; + virtual void mousemove_event(GUI::MouseEvent&) override; + virtual void mouseup_event(GUI::MouseEvent&) override; + virtual void mousedown_event(GUI::MouseEvent&) override; + + void set_toggling_cells(bool toggling) + { + m_toggling_cells = toggling; + if (!toggling) + m_last_cell_toggled = m_board->total_size(); + } + + size_t last_toggled() const { return m_last_cell_toggled; } + bool is_toggling() const { return m_toggling_cells; } + + void toggle_cell(size_t index); + void clear_cells() { m_board->clear(); } + void randomize_cells() { m_board->randomize(); } + + int get_cell_size() const; + Gfx::IntSize get_board_offset() const; + + size_t get_index_for_point(int x, int y) const; + + void update_board(size_t rows, size_t columns); + const Board* board() const { return m_board.ptr(); } + + bool is_running() const { return m_running; } + void set_running(bool r); + + void set_toolbar_enabled(bool); + + void run_generation(); + + int running_timer_interval() const { return m_running_timer_interval; } + void set_running_timer_interval(int interval); + + Function<void()> on_running_state_change; + Function<void()> on_stall; + Function<void(Board*, size_t)> on_cell_toggled; + +private: + BoardWidget(size_t rows, size_t columns); + + bool m_toggling_cells { false }; + size_t m_last_cell_toggled { 0 }; + + OwnPtr<Board> m_board { nullptr }; + + bool m_running { false }; + + int m_running_timer_interval { 500 }; + RefPtr<Core::Timer> m_timer; +}; diff --git a/Userland/Games/GameOfLife/CMakeLists.txt b/Userland/Games/GameOfLife/CMakeLists.txt new file mode 100644 index 0000000000..7ea612fd5d --- /dev/null +++ b/Userland/Games/GameOfLife/CMakeLists.txt @@ -0,0 +1,11 @@ +compile_gml(GameOfLife.gml GameOfLifeGML.h game_of_life_gml) + +set(SOURCES + Board.cpp + BoardWidget.cpp + GameOfLifeGML.h + main.cpp +) + +serenity_app(GameOfLife ICON app-gameoflife) +target_link_libraries(GameOfLife LibGUI) diff --git a/Userland/Games/GameOfLife/GameOfLife.gml b/Userland/Games/GameOfLife/GameOfLife.gml new file mode 100644 index 0000000000..4a0d0803f1 --- /dev/null +++ b/Userland/Games/GameOfLife/GameOfLife.gml @@ -0,0 +1,57 @@ +@GUI::Widget { + layout: @GUI::VerticalBoxLayout { + } + + @GUI::ToolbarContainer { + + @GUI::Toolbar { + name: "toolbar" + + @GUI::Label { + text: "Columns:" + fixed_width: 60 + } + + @GUI::SpinBox { + name: "columns_spinbox" + min: 10 + max: 999 + fixed_width: 40 + } + @GUI::VerticalSeparator { + } + @GUI::Label { + text: "Rows:" + fixed_width: 40 + } + + @GUI::SpinBox { + name: "rows_spinbox" + min: 10 + max: 999 + fixed_width: 40 + } + @GUI::VerticalSeparator { + } + @GUI::Label { + text: "Update Speed:" + fixed_width: 90 + } + @GUI::SpinBox { + name: "interval_spinbox" + min: 10 + max: 5000 + fixed_width: 60 + } + } + } + + @GUI::Widget { + name: "board_widget_container" + fill_with_background_color: true + } + + @GUI::Statusbar { + name: "statusbar" + } +} diff --git a/Userland/Games/GameOfLife/main.cpp b/Userland/Games/GameOfLife/main.cpp new file mode 100644 index 0000000000..0fc71f0bbe --- /dev/null +++ b/Userland/Games/GameOfLife/main.cpp @@ -0,0 +1,170 @@ +/* + * Copyright (c) 2021, Andres Crucitti <dasc495@gmail.com> + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include "BoardWidget.h" +#include <Games/GameOfLife/GameOfLifeGML.h> +#include <LibGUI/Application.h> +#include <LibGUI/BoxLayout.h> +#include <LibGUI/Button.h> +#include <LibGUI/Icon.h> +#include <LibGUI/Label.h> +#include <LibGUI/Menu.h> +#include <LibGUI/Menubar.h> +#include <LibGUI/MessageBox.h> +#include <LibGUI/Slider.h> +#include <LibGUI/SpinBox.h> +#include <LibGUI/Statusbar.h> +#include <LibGUI/Toolbar.h> +#include <LibGUI/ToolbarContainer.h> +#include <LibGUI/Window.h> + +const char* click_tip = "Tip: click the board to toggle individual cells, or click+drag to toggle multiple cells"; + +int main(int argc, char** argv) +{ + auto app = GUI::Application::construct(argc, argv); + auto app_icon = GUI::Icon::default_icon("app-gameoflife"); + + auto window = GUI::Window::construct(); + window->set_icon(app_icon.bitmap_for_size(16)); + + size_t board_columns = 35; + size_t board_rows = 35; + + window->set_double_buffering_enabled(false); + window->set_title("Game Of Life"); + + auto& main_widget = window->set_main_widget<GUI::Widget>(); + main_widget.load_from_gml(game_of_life_gml); + main_widget.set_fill_with_background_color(true); + + auto& main_toolbar = *main_widget.find_descendant_of_type_named<GUI::Toolbar>("toolbar"); + + auto& board_widget_container = *main_widget.find_descendant_of_type_named<GUI::Widget>("board_widget_container"); + auto& board_layout = board_widget_container.set_layout<GUI::VerticalBoxLayout>(); + board_layout.set_spacing(0); + auto& board_widget = board_widget_container.add<BoardWidget>(board_rows, board_columns); + board_widget.randomize_cells(); + + auto& statusbar = *main_widget.find_descendant_of_type_named<GUI::Statusbar>("statusbar"); + statusbar.set_text(click_tip); + + auto& columns_spinbox = *main_widget.find_descendant_of_type_named<GUI::SpinBox>("columns_spinbox"); + auto& rows_spinbox = *main_widget.find_descendant_of_type_named<GUI::SpinBox>("rows_spinbox"); + + columns_spinbox.set_value(board_columns); + rows_spinbox.set_value(board_rows); + + auto size_changed_function = [&] { + statusbar.set_text(click_tip); + board_widget.update_board(rows_spinbox.value(), columns_spinbox.value()); + board_widget.randomize_cells(); + board_widget.update(); + }; + + rows_spinbox.on_change = [&](auto) { size_changed_function(); }; + columns_spinbox.on_change = [&](auto) { size_changed_function(); }; + + auto& interval_spinbox = *main_widget.find_descendant_of_type_named<GUI::SpinBox>("interval_spinbox"); + + interval_spinbox.on_change = [&](auto value) { + board_widget.set_running_timer_interval(value); + }; + + interval_spinbox.set_value(150); + + auto interval_label = GUI::Label::construct(); + interval_label->set_fixed_width(15); + interval_label->set_text("ms"); + + main_toolbar.add_child(interval_label); + + auto paused_icon = Gfx::Bitmap::load_from_file("/res/icons/16x16/pause.png"); + auto play_icon = Gfx::Bitmap::load_from_file("/res/icons/16x16/play.png"); + + auto toggle_running_action = GUI::Action::create("Toggle Running", { Mod_None, Key_Return }, *play_icon, [&](GUI::Action&) { + board_widget.set_running(!board_widget.is_running()); + }); + + toggle_running_action->set_checkable(true); + main_toolbar.add_action(toggle_running_action); + + auto run_one_generation_action = GUI::Action::create("Run Next Generation", { Mod_Ctrl, Key_Equal }, Gfx::Bitmap::load_from_file("/res/icons/16x16/go-forward.png"), [&](const GUI::Action&) { + statusbar.set_text(click_tip); + board_widget.run_generation(); + }); + main_toolbar.add_action(run_one_generation_action); + + auto clear_board_action = GUI::Action::create("Clear board", { Mod_Ctrl, Key_N }, Gfx::Bitmap::load_from_file("/res/icons/16x16/delete.png"), [&](auto&) { + statusbar.set_text(click_tip); + board_widget.clear_cells(); + board_widget.update(); + }); + main_toolbar.add_action(clear_board_action); + + auto randomize_cells_action = GUI::Action::create("Randomize board", { Mod_Ctrl, Key_R }, Gfx::Bitmap::load_from_file("/res/icons/16x16/reload.png"), [&](auto&) { + statusbar.set_text(click_tip); + board_widget.randomize_cells(); + board_widget.update(); + }); + main_toolbar.add_action(randomize_cells_action); + + auto menubar = GUI::Menubar::construct(); + auto& app_menu = menubar->add_menu("Game Of Life"); + + app_menu.add_action(clear_board_action); + app_menu.add_action(randomize_cells_action); + app_menu.add_separator(); + app_menu.add_action(toggle_running_action); + app_menu.add_action(run_one_generation_action); + + 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("GameOfLife", app_icon, window)); + + window->set_menubar(move(menubar)); + + board_widget.on_running_state_change = [&]() { + if (board_widget.is_running()) { + statusbar.set_text("Running..."); + toggle_running_action->set_icon(paused_icon); + main_widget.set_override_cursor(Gfx::StandardCursor::None); + } else { + statusbar.set_text(click_tip); + toggle_running_action->set_icon(play_icon); + main_widget.set_override_cursor(Gfx::StandardCursor::Drag); + } + + interval_spinbox.set_value(board_widget.running_timer_interval()); + + rows_spinbox.set_enabled(!board_widget.is_running()); + columns_spinbox.set_enabled(!board_widget.is_running()); + interval_spinbox.set_enabled(!board_widget.is_running()); + + run_one_generation_action->set_enabled(!board_widget.is_running()); + clear_board_action->set_enabled(!board_widget.is_running()); + randomize_cells_action->set_enabled(!board_widget.is_running()); + + board_widget.update(); + }; + + board_widget.on_stall = [&] { + toggle_running_action->activate(); + statusbar.set_text("Stalled..."); + }; + + board_widget.on_cell_toggled = [&](auto, auto) { + statusbar.set_text(click_tip); + }; + + window->resize(500, 420); + window->show(); + + return app->exec(); +} |