From 1965d60aebf3ee9cc4631b277eaf37bc8edd92f2 Mon Sep 17 00:00:00 2001 From: Ryan Wilson Date: Sat, 22 May 2021 05:54:58 -0300 Subject: [PATCH] GameOfLife: Add choosable patterns --- Userland/Games/GameOfLife/BoardWidget.cpp | 218 +++++++++++++++++++++- Userland/Games/GameOfLife/BoardWidget.h | 23 +++ Userland/Games/GameOfLife/CMakeLists.txt | 3 +- Userland/Games/GameOfLife/Pattern.cpp | 39 ++++ Userland/Games/GameOfLife/Pattern.h | 26 +++ Userland/Games/GameOfLife/main.cpp | 7 + 6 files changed, 309 insertions(+), 7 deletions(-) create mode 100644 Userland/Games/GameOfLife/Pattern.cpp create mode 100644 Userland/Games/GameOfLife/Pattern.h diff --git a/Userland/Games/GameOfLife/BoardWidget.cpp b/Userland/Games/GameOfLife/BoardWidget.cpp index a8a7c1fb45..aca006bf9b 100644 --- a/Userland/Games/GameOfLife/BoardWidget.cpp +++ b/Userland/Games/GameOfLife/BoardWidget.cpp @@ -1,22 +1,36 @@ /* * Copyright (c) 2021, Andres Crucitti * Copyright (c) 2021, Linus Groh + * Copyright (c) 2021, Ryan Wilson * * SPDX-License-Identifier: BSD-2-Clause */ #include "BoardWidget.h" +#include #include BoardWidget::BoardWidget(size_t rows, size_t columns) : m_board(make(rows, columns)) { m_timer = add(); + m_pattern_preview_timer = add(); m_timer->stop(); + m_pattern_preview_timer->stop(); m_timer->on_timeout = [this] { run_generation(); }; + m_pattern_preview_timer->on_timeout = [this] { + update(); + }; m_timer->set_interval(m_running_timer_interval); + m_pattern_preview_timer->set_interval(m_running_pattern_preview_timer_interval); + + on_pattern_selection = [this](auto* pattern) { + m_selected_pattern = pattern; + }; + + setup_patterns(); } void BoardWidget::run_generation() @@ -128,6 +142,20 @@ void BoardWidget::paint_event(GUI::PaintEvent& event) fill_color = Color::MidGray; } + if (m_selected_pattern != nullptr) { + int y_offset = 0; + for (auto line : m_selected_pattern->pattern()) { + int x_offset = 0; + for (auto c : line) { + if (c == 'O' && (m_last_cell_hovered.row + y_offset) < m_board->rows() + && (m_last_cell_hovered.column + x_offset) < m_board->columns() && row == (m_last_cell_hovered.row + y_offset) && column == (m_last_cell_hovered.column + x_offset)) + fill_color = Color::Green; + x_offset++; + } + y_offset++; + } + } + painter.fill_rect(cell_rect, fill_color); if (cell_size > 4) { painter.draw_rect(cell_rect, border_color); @@ -138,12 +166,32 @@ void BoardWidget::paint_event(GUI::PaintEvent& event) void BoardWidget::mousedown_event(GUI::MouseEvent& event) { - set_toggling_cells(true); - auto row_and_column = get_row_and_column_for_point(event.x(), event.y()); - if (!row_and_column.has_value()) - return; - auto [row, column] = row_and_column.value(); - toggle_cell(row, column); + if (event.button() == GUI::MouseButton::Left) { + set_toggling_cells(true); + auto row_and_column = get_row_and_column_for_point(event.x(), event.y()); + if (!row_and_column.has_value()) + return; + auto [row, column] = row_and_column.value(); + if (m_selected_pattern == nullptr) + toggle_cell(row, column); + else + place_pattern(row, column); + } +} + +void BoardWidget::context_menu_event(GUI::ContextMenuEvent& event) +{ + if (!m_context_menu) { + m_context_menu = GUI::Menu::construct(); + + auto& insert_pattern_menu = m_context_menu->add_submenu("&Insert Pattern"); + for_each_pattern([&](auto& pattern) { + if (pattern.action()) + insert_pattern_menu.add_action(*pattern.action()); + }); + } + if (!m_running) + m_context_menu->popup(event.screen_position()); } void BoardWidget::mousemove_event(GUI::MouseEvent& event) @@ -156,6 +204,11 @@ void BoardWidget::mousemove_event(GUI::MouseEvent& event) if (m_last_cell_toggled.row != row || m_last_cell_toggled.column != column) toggle_cell(row, column); } + m_last_cell_hovered = { row, column }; + if (m_selected_pattern != nullptr) { + if (!m_pattern_preview_timer->is_active()) + m_pattern_preview_timer->start(); + } } void BoardWidget::mouseup_event(GUI::MouseEvent&) @@ -178,3 +231,156 @@ Optional BoardWidget::get_row_and_column_for_point(int x, i .column = static_cast((x - board_offset.width()) / cell_size), } }; } + +void BoardWidget::place_pattern(size_t row, size_t column) +{ + int y_offset = 0; + for (auto line : m_selected_pattern->pattern()) { + int x_offset = 0; + for (auto c : line) { + if (c == 'O' && (row + y_offset) < m_board->rows() && (column + x_offset) < m_board->columns()) + toggle_cell(row + y_offset, column + x_offset); + x_offset++; + } + y_offset++; + } + m_selected_pattern = nullptr; + if (m_pattern_preview_timer->is_active()) + m_pattern_preview_timer->stop(); +} + +void BoardWidget::setup_patterns() +{ + auto add_pattern = [&](String name, NonnullOwnPtr pattern) { + auto action = GUI::Action::create(move(name), [this, pattern = pattern.ptr()](const GUI::Action&) { + on_pattern_selection(pattern); + }); + pattern->set_action(action); + m_patterns.append(move(pattern)); + }; + + Vector blinker = { + "OOO" + }; + + Vector toad = { + ".OOO", + "OOO." + }; + + Vector glider = { + ".O.", + "..O", + "OOO", + }; + + Vector lightweight_spaceship = { + ".OO..", + "OOOO.", + "OO.OO", + "..OO." + }; + + Vector middleweight_spaceship = { + ".OOOOO", + "O....O", + ".....O", + "O...O.", + "..O..." + }; + + Vector heavyweight_spaceship = { + "..OO...", + "O....O.", + "......O", + "O.....O", + ".OOOOOO" + }; + + Vector infinite_1 = { "OOOOOOOO.OOOOO...OOO......OOOOOOO.OOOOO" }; + + Vector infinite_2 = { + "......O.", + "....O.OO", + "....O.O.", + "....O...", + "..O.....", + "O.O....." + }; + + Vector infinite_3 = { + "OOO.O", + "O....", + "...OO", + ".OO.O", + "O.O.O" + }; + + Vector simkin_glider_gun = { + "OO.....OO........................", + "OO.....OO........................", + ".................................", + "....OO...........................", + "....OO...........................", + ".................................", + ".................................", + ".................................", + ".................................", + "......................OO.OO......", + ".....................O.....O.....", + ".....................O......O..OO", + ".....................OOO...O...OO", + "..........................O......", + ".................................", + ".................................", + ".................................", + "....................OO...........", + "....................O............", + ".....................OOO.........", + ".......................O........." + }; + Vector gosper_glider_gun = { + "........................O...........", + "......................O.O...........", + "............OO......OO............OO", + "...........O...O....OO............OO", + "OO........O.....O...OO..............", + "OO........O...O.OO....O.O...........", + "..........O.....O.......O...........", + "...........O...O....................", + "............OO......................" + }; + + Vector r_pentomino = { + ".OO", + "OO.", + ".O." + }; + + Vector diehard = { + "......O.", + "OO......", + ".O...OOO" + }; + + Vector acorn = { + ".O.....", + "...O...", + "OO..OOO" + }; + + add_pattern("Blinker", make(move(blinker))); + add_pattern("Toad", make(move(toad))); + add_pattern("Glider", make(move(glider))); + add_pattern("Lightweight Spaceship", make(move(lightweight_spaceship))); + add_pattern("Middleweight Spaceship", make(move(middleweight_spaceship))); + add_pattern("Heavyweight Spaceship", make(move(heavyweight_spaceship))); + add_pattern("Infinite 1", make(move(infinite_1))); + add_pattern("Infinite 2", make(move(infinite_2))); + add_pattern("Infinite 3", make(move(infinite_3))); + add_pattern("R-Pentomino", make(move(r_pentomino))); + add_pattern("Diehard", make(move(diehard))); + add_pattern("Acorn", make(move(acorn))); + add_pattern("Simkin's Glider Gun", make(move(simkin_glider_gun))); + add_pattern("Gosper's Glider Gun", make(move(gosper_glider_gun))); +} diff --git a/Userland/Games/GameOfLife/BoardWidget.h b/Userland/Games/GameOfLife/BoardWidget.h index 7933c96e8f..b72b4b1088 100644 --- a/Userland/Games/GameOfLife/BoardWidget.h +++ b/Userland/Games/GameOfLife/BoardWidget.h @@ -1,6 +1,7 @@ /* * Copyright (c) 2021, Andres Crucitti * Copyright (c) 2021, Linus Groh + * Copyright (c) 2021, Ryan Wilson * * SPDX-License-Identifier: BSD-2-Clause */ @@ -8,11 +9,13 @@ #pragma once #include "Board.h" +#include "Pattern.h" #include #include #include #include #include +#include #include class BoardWidget final : public GUI::Widget { @@ -23,6 +26,7 @@ public: virtual void mousemove_event(GUI::MouseEvent&) override; virtual void mouseup_event(GUI::MouseEvent&) override; virtual void mousedown_event(GUI::MouseEvent&) override; + virtual void context_menu_event(GUI::ContextMenuEvent&) override; void set_toggling_cells(bool toggling) { @@ -46,6 +50,15 @@ public: bool is_running() const { return m_running; } void set_running(bool r); + Pattern* selected_pattern() { return m_selected_pattern; } + Function on_pattern_selection; + template + void for_each_pattern(Callback callback) + { + for (auto& pattern : m_patterns) + callback(pattern); + } + void run_generation(); int running_timer_interval() const { return m_running_timer_interval; } @@ -57,14 +70,24 @@ public: private: BoardWidget(size_t rows, size_t columns); + void setup_patterns(); + void place_pattern(size_t row, size_t column); bool m_toggling_cells { false }; Board::RowAndColumn m_last_cell_toggled {}; + Board::RowAndColumn m_last_cell_hovered {}; + Pattern* m_selected_pattern { nullptr }; + NonnullOwnPtrVector m_patterns; NonnullOwnPtr m_board; bool m_running { false }; int m_running_timer_interval { 500 }; + int m_running_pattern_preview_timer_interval { 100 }; + + RefPtr m_context_menu; + RefPtr m_timer; + RefPtr m_pattern_preview_timer; }; diff --git a/Userland/Games/GameOfLife/CMakeLists.txt b/Userland/Games/GameOfLife/CMakeLists.txt index 7ea612fd5d..225e4f1b8a 100644 --- a/Userland/Games/GameOfLife/CMakeLists.txt +++ b/Userland/Games/GameOfLife/CMakeLists.txt @@ -1,10 +1,11 @@ compile_gml(GameOfLife.gml GameOfLifeGML.h game_of_life_gml) set(SOURCES + main.cpp Board.cpp BoardWidget.cpp GameOfLifeGML.h - main.cpp + Pattern.cpp ) serenity_app(GameOfLife ICON app-gameoflife) diff --git a/Userland/Games/GameOfLife/Pattern.cpp b/Userland/Games/GameOfLife/Pattern.cpp new file mode 100644 index 0000000000..35376c6375 --- /dev/null +++ b/Userland/Games/GameOfLife/Pattern.cpp @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2021, Ryan Wilson + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include "Pattern.h" +#include +#include +#include +#include +#include + +Pattern::Pattern(Vector pattern) +{ + m_pattern = move(pattern); +} + +Pattern::~Pattern() +{ +} + +void Pattern::set_action(GUI::Action* action) +{ + m_action = action; +} + +void Pattern::rotate_clockwise() +{ + Vector rotated; + for (size_t i = 0; i < m_pattern.first().length(); i++) { + StringBuilder builder; + for (int j = m_pattern.size() - 1; j >= 0; j--) { + builder.append(m_pattern.at(j).substring(i, 1)); + } + rotated.append(builder.to_string()); + } + m_pattern = move(rotated); +} diff --git a/Userland/Games/GameOfLife/Pattern.h b/Userland/Games/GameOfLife/Pattern.h new file mode 100644 index 0000000000..9fa3ce1c8b --- /dev/null +++ b/Userland/Games/GameOfLife/Pattern.h @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2021, Ryan Wilson + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include "Board.h" +#include +#include +#include + +class Pattern { +public: + Pattern(Vector); + virtual ~Pattern(); + Vector pattern() { return m_pattern; }; + GUI::Action* action() { return m_action; } + void set_action(GUI::Action*); + void rotate_clockwise(); + +private: + RefPtr m_action; + Vector m_pattern; +}; diff --git a/Userland/Games/GameOfLife/main.cpp b/Userland/Games/GameOfLife/main.cpp index aebb038bcd..bbf0846b7d 100644 --- a/Userland/Games/GameOfLife/main.cpp +++ b/Userland/Games/GameOfLife/main.cpp @@ -15,6 +15,7 @@ #include #include #include +#include #include const char* click_tip = "Tip: click the board to toggle individual cells, or click+drag to toggle multiple cells"; @@ -102,6 +103,12 @@ int main(int argc, char** argv) }); main_toolbar.add_action(randomize_cells_action); + auto rotate_pattern_action = GUI::Action::create("&Rotate pattern", { 0, Key_R }, Gfx::Bitmap::load_from_file("/res/icons/16x16/redo.png"), [&](auto&) { + if (board_widget.selected_pattern() != nullptr) + board_widget.selected_pattern()->rotate_clockwise(); + }); + main_toolbar.add_action(rotate_pattern_action); + auto menubar = GUI::Menubar::construct(); auto& game_menu = menubar->add_menu("&Game");