summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Base/res/apps/Conway.af4
-rw-r--r--Base/res/apps/GameOfLife.af4
-rw-r--r--Base/res/icons/16x16/app-conway.pngbin197 -> 0 bytes
-rw-r--r--Base/res/icons/16x16/app-gameoflife.pngbin0 -> 115 bytes
-rw-r--r--Base/res/icons/32x32/app-conway.pngbin275 -> 0 bytes
-rw-r--r--Base/res/icons/32x32/app-gameoflife.pngbin0 -> 139 bytes
-rw-r--r--Userland/Games/CMakeLists.txt2
-rw-r--r--Userland/Games/Conway/CMakeLists.txt7
-rw-r--r--Userland/Games/Conway/Game.cpp163
-rw-r--r--Userland/Games/Conway/Game.h41
-rw-r--r--Userland/Games/Conway/main.cpp72
-rw-r--r--Userland/Games/GameOfLife/Board.cpp132
-rw-r--r--Userland/Games/GameOfLife/Board.h50
-rw-r--r--Userland/Games/GameOfLife/BoardWidget.cpp172
-rw-r--r--Userland/Games/GameOfLife/BoardWidget.h70
-rw-r--r--Userland/Games/GameOfLife/CMakeLists.txt11
-rw-r--r--Userland/Games/GameOfLife/GameOfLife.gml57
-rw-r--r--Userland/Games/GameOfLife/main.cpp170
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
deleted file mode 100644
index ca39bc1866..0000000000
--- a/Base/res/icons/16x16/app-conway.png
+++ /dev/null
Binary files differ
diff --git a/Base/res/icons/16x16/app-gameoflife.png b/Base/res/icons/16x16/app-gameoflife.png
new file mode 100644
index 0000000000..158a774090
--- /dev/null
+++ b/Base/res/icons/16x16/app-gameoflife.png
Binary files differ
diff --git a/Base/res/icons/32x32/app-conway.png b/Base/res/icons/32x32/app-conway.png
deleted file mode 100644
index af831a8996..0000000000
--- a/Base/res/icons/32x32/app-conway.png
+++ /dev/null
Binary files differ
diff --git a/Base/res/icons/32x32/app-gameoflife.png b/Base/res/icons/32x32/app-gameoflife.png
new file mode 100644
index 0000000000..9360efa7bb
--- /dev/null
+++ b/Base/res/icons/32x32/app-gameoflife.png
Binary files differ
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();
+}