commit 25d9b22fbd03222a8ef7f16903fb0066e92f97bf Author: lejulien Date: Fri Jan 9 10:45:23 2026 +0100 Copying from gitlab diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ff58695 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +build +enc_temp_folder +out +_deps +CMakeSettings.json +.vs +.cache diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..5f8e6c9 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,85 @@ +cmake_minimum_required(VERSION 3.18) +set(NAME "GameOfLifeEditor") +project(${NAME} CXX) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) +set(CMAKE_EXPORT_COMPILE_COMMANDS ON) + +if(WIN32) + set(IMGUI_BACKEND "imgui_impl_dx11.cpp") # DirectX 11 for Windows +elseif(APPLE) + set(IMGUI_BACKEND "imgui_impl_metal.mm") # Metal for macOS +elseif(UNIX) + set(IMGUI_BACKEND "imgui_impl_opengl3.cpp") # OpenGL for Linux +endif() + +# includes +include_directories(./includes) + +# raylib +include(FetchContent) +FetchContent_Declare( + raylib + GIT_REPOSITORY https://github.com/raysan5/raylib.git + GIT_TAG 5.5 + GIT_SHALLOW 1 +) +FetchContent_GetProperties(raylib) +if (NOT raylib_POPULATED) + set(FETCHCONTENT_QUIET NO) + FetchContent_Populate(raylib) + set(BUILD_EXAMPLES OFF CACHE BOOL "" FORCE) + set(BUILD_GAMES OFF CACHE BOOL "" FORCE) + add_subdirectory(${raylib_SOURCE_DIR} ${raylib_BINARY_DIR}) +endif() + +# imgui +FetchContent_Declare( + imgui + GIT_REPOSITORY https://github.com/ocornut/imgui.git + GIT_TAG docking +) + +FetchContent_MakeAvailable(imgui) + +set(imgui_SOURCE_DIR ${CMAKE_BINARY_DIR}/_deps/imgui-src) + +# rlimgui +FetchContent_Declare( + rlImGui + GIT_REPOSITORY https://github.com/raylib-extras/rlImGui.git + GIT_TAG main +) +FetchContent_MakeAvailable(rlImGui) +set(rlImGui_SOURCE_DIR ${CMAKE_BINARY_DIR}/_deps/rlimgui-src) + + +# nlohmann::json +FetchContent_Declare( + json + GIT_REPOSITORY https://github.com/nlohmann/json.git + GIT_TAG v3.11.3 +) +FetchContent_MakeAvailable(json) + +include_directories(${imgui_SOURCE_DIR} ${rlImGui_SOURCE_DIR}) + +set(SRC_CXX_FILES "./src/main.cpp" + "./src/rules.cpp" + "./src/world.cpp" + "./src/render.cpp" + "${rlImGui_SOURCE_DIR}/rlImGui.cpp" + "${imgui_SOURCE_DIR}/imgui.cpp" + "${imgui_SOURCE_DIR}/imgui_draw.cpp" + "${imgui_SOURCE_DIR}/imgui_widgets.cpp" + "${imgui_SOURCE_DIR}/imgui_tables.cpp" + "${imgui_SOURCE_DIR}/backends/${IMGUI_BACKEND}" +) + +# Setup the example +add_executable(${NAME} ${SRC_CXX_FILES}) + +# Link raylib and raylib-cpp +target_link_libraries(${NAME} PUBLIC raylib nlohmann_json::nlohmann_json) diff --git a/compile_commands.json b/compile_commands.json new file mode 120000 index 0000000..affbd32 --- /dev/null +++ b/compile_commands.json @@ -0,0 +1 @@ +./build/compile_commands.json \ No newline at end of file diff --git a/includes/render.hpp b/includes/render.hpp new file mode 100644 index 0000000..3332b33 --- /dev/null +++ b/includes/render.hpp @@ -0,0 +1,29 @@ +/* ************************************************************************** */ +/* */ +/* / ) */ +/* render.hpp (\__/) ( ( */ +/* ) ( ) ) */ +/* By: lejulien ={ }= / / */ +/* ) `-------/ / */ +/* Created: 2023/01/09 12:44:54 by lejulien ( / */ +/* Updated: 2023/01/14 16:51:15 by lejulien \ | */ +/* */ +/* ************************************************************************** */ + +#include "world.hpp" + +#pragma once + +class Render { +public: + // Constructor + Render(int cell_size); + + // Member function + void display(World *world); + void updateCellSize(int new_size); + +private: + void display_world(std::vector *data, int width, int height); + int cell_size_; +}; diff --git a/includes/rules.hpp b/includes/rules.hpp new file mode 100644 index 0000000..5ae7bef --- /dev/null +++ b/includes/rules.hpp @@ -0,0 +1,35 @@ +/* ************************************************************************** */ +/* */ +/* / ) */ +/* rules.hpp (\__/) ( ( */ +/* ) ( ) ) */ +/* By: lejulien ={ }= / / */ +/* ) `-------/ / */ +/* Created: 2023/01/09 12:16:47 by lejulien ( / */ +/* Updated: 2023/01/14 16:47:04 by lejulien \ | */ +/* */ +/* ************************************************************************** */ + +#include "world.hpp" + +#pragma once + +class Rules { +private: + void ortho_neighbors(int &neighbors, int i, int j); + void diag_neighbors(int &neighbors, int i, int j); + bool is_alive(int i, int j); + void offset_coord(int &i, int &j); + +public: + Rules(); + void setup(World *world); + void newWorld(World *world); + void update(); + +private: + World *_world; + std::vector _buffer; + int _width; + int _height; +}; diff --git a/includes/types.hpp b/includes/types.hpp new file mode 100644 index 0000000..eec79a2 --- /dev/null +++ b/includes/types.hpp @@ -0,0 +1,12 @@ +#pragma once + +enum class MenuState { + NONE, + PLAY, + EDIT, +}; + +typedef struct { + int x; + int y; +} Vector2i; diff --git a/includes/world.hpp b/includes/world.hpp new file mode 100644 index 0000000..ec97798 --- /dev/null +++ b/includes/world.hpp @@ -0,0 +1,45 @@ +#include +#include +#include +#include +#include +#include + +#include "../includes/types.hpp" + +#pragma once + +#define MAX_HISTORY_SIZE 100 + +class World { + +public: + World(int width, int height); + ~World(); + + std::vector *getWorldData(); + int getWidth(); + int getHeight(); + void randomize(); + void saveCompressed(); + size_t getCycle() const { return _cycle_index; } + void stepBack(); + void clear(); + void setCell(int x, int y); + void resize(int width, int height); // destructive + std::vector getSelection(Vector2i &origin, Vector2i &size); + + // Private members +private: + std::vector getCompressed(); + void loadCompressed(const std::vector &compressed); + + std::vector *_data; + size_t _cycle_index = 0; + std::vector> _history = {}; + + // Private data +private: + int _width; + int _height; +}; diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..b30b652 --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,380 @@ +#include +#include +#include +#include +#include + +#include "../includes/render.hpp" +#include "../includes/rules.hpp" +#include "../includes/world.hpp" +#include "../includes/types.hpp" +#include "imgui.h" +#include "raylib.h" +#include "rlImGui.h" + + +Vector2 snapToGrid(Vector2 screen, int cell_size) { + return {static_cast(round(screen.x / cell_size) * cell_size), + static_cast(round(screen.y / cell_size) * cell_size)}; +} + +Vector2i screenToGrid(Vector2 screen, int cell_size) { + return {static_cast(round(screen.x / cell_size)), + static_cast(round(screen.y / cell_size))}; +} + +int main(int ac, char **av) { + // Load or default config + const std::string config_file_name = "config.json"; + nlohmann::json config_json; + MenuState menu_state = MenuState::NONE; + + std::fstream config_file(config_file_name, std::ios::in | std::ios::out); + + if (!config_file.is_open()) { + config_file.open(config_file_name, std::ios::out); + if (!config_file) { + std::cerr << "Failed to create config file, the settings will not be " + "kepts after restart." + << std::endl; + return 1; + } + config_file.close(); + config_file.open(config_file_name, std::ios::in | std::ios::out); + config_file << "{}"; + config_file.flush(); + } + // Try reading the configuration + try { + config_file.seekg(0); + config_file >> config_json; + } catch (const nlohmann::json::parse_error &e) { + std::cerr << "An error occured while loading config : " << e.what() + << std::endl; + std::cerr << "Please fix the configuration or remove the config.json file." + << std::endl; + return 1; + } + config_file.close(); + + // Check config values or populate them + if (!config_json.contains("cell_size") || + (config_json["cell_size"] < 4 || config_json["cell_size"] > 100)) { + config_json["cell_size"] = 10; + } + if (!config_json.contains("screen_width") || + (config_json["screen_width"] < 800 || + config_json["screen_width"] > 1920)) { + config_json["screen_width"] = 800; + } + if (!config_json.contains("screen_height") || + (config_json["screen_height"] < 600 || + config_json["screen_height"] > 1200)) { + config_json["screen_height"] = 600; + } + if (!config_json.contains("dark_theme")) { + config_json["dark_theme"] = true; + } + if (!config_json.contains("fps") || + (config_json["fps"] < 0 || config_json["fps"] > 30)) { + config_json["fps"] = 800; + } + + InitWindow(config_json["screen_width"], config_json["screen_height"], + &av[0][2]); + + SetTargetFPS(60); + + rlImGuiSetup(config_json["dark_theme"]); + // Selection window + + RenderTexture2D selectionTexture = LoadRenderTexture(200, 200); + + // Initialize objects + World world(config_json["screen_width"].get() / + config_json["cell_size"].get(), + config_json["screen_height"].get() / + config_json["cell_size"].get()); + Rules rules = Rules(); + Render render(config_json["cell_size"]); + + // Imgui control menu + int fps_ctrl = config_json["fps"].get(); + int cell_size_ctrl = config_json["cell_size"].get(); + bool play_ctrl = true; + bool step_ctrl = false; + bool step_back_ctrl = false; + bool rand_ctrl = false; + bool edit_ctrl = false; + bool clear_ctrl = false; + bool settings_window = false; + int width_ctrl = config_json["screen_width"].get(); + int height_ctrl = config_json["screen_height"].get(); + bool dark_theme_ctrl = config_json["dark_theme"].get(); + bool apply_ctrl = false; + + // Speed handling values + float sim_speed = 1.0f; + float deltaTimeAccumulator = 0.0f; + float timePerUpdate = 1.0f / 10.0f; + + // Selection window + Vector2 sel_pos = {0., 0.}; + bool selecting = false; + std::vector sel_data = {}; + bool sel_ctrl = false; + char patern_name[255]; + patern_name[0] = '\0'; + std::string sel_txt_input_hint("patern name"); + + // Setups + rules.setup(&world); + // Diplay generations + while (!WindowShouldClose()) { + if (menu_state == MenuState::PLAY || menu_state == MenuState::EDIT) { + if (!play_ctrl && !edit_ctrl) { + menu_state = MenuState::NONE; + } + } + // Frames shinenigans + float deltaTime = GetFrameTime(); + sim_speed = fps_ctrl / 10.0f; + timePerUpdate = (1.0f / 10.0f) / sim_speed; + + auto gesture = GetGestureDetected(); + auto mousePos = GetMousePosition(); + if (rand_ctrl) { + world.randomize(); + rand_ctrl = false; + } + if (clear_ctrl) { + world.clear(); + clear_ctrl = false; + } + if (edit_ctrl && play_ctrl) { + if (menu_state == MenuState::PLAY) { + menu_state = MenuState::EDIT; + play_ctrl = false; + } else if (menu_state == MenuState::EDIT) { + menu_state = MenuState::PLAY; + edit_ctrl = false; + } + } else if (play_ctrl) { + menu_state = MenuState::PLAY; + } else if (edit_ctrl) { + menu_state = MenuState::EDIT; + } + if (edit_ctrl && IsMouseButtonPressed(0) && + !ImGui::IsWindowHovered(ImGuiHoveredFlags_AnyWindow)) { + menu_state = MenuState::EDIT; + world.setCell(mousePos.x / config_json["cell_size"].get(), + mousePos.y / config_json["cell_size"].get()); + } + // Selection behaviour + if (!edit_ctrl && !play_ctrl) { + if (gesture == GESTURE_TAP && !ImGui::IsWindowHovered(ImGuiHoveredFlags_AnyWindow)) { + sel_pos = snapToGrid(mousePos, cell_size_ctrl); + selecting = true; + } + if (ImGui::IsMouseReleased(0) && selecting == true && mousePos.x >=0 && mousePos.x < width_ctrl && mousePos.y >=0 && mousePos.y < height_ctrl ) { + selecting = false; + Vector2i p1 = screenToGrid(sel_pos, cell_size_ctrl); + Vector2i p2 = screenToGrid(mousePos, cell_size_ctrl); + // Get origin + Vector2i orig = { + (p1.x < p2.x)?p1.x:p2.x, + (p1.y < p2.y)?p1.y:p2.y, + }; + // Get selection size + Vector2i sel_size = { + (p1.x > p2.x)?p1.x-p2.x:p2.x-p1.x, + (p1.y > p2.y)?p1.y-p2.y:p2.y-p1.y, + }; + // Ensure there is at least one cell selected + if (!(sel_size.x == 0 || sel_size.y == 0)) { + sel_data = std::move(world.getSelection(orig, sel_size)); + sel_ctrl = true; + } + } + } + if (!sel_ctrl) { + patern_name[0] = '\0'; + } + if (step_ctrl) { + world.saveCompressed(); + rules.update(); + step_ctrl = false; + } + if (step_back_ctrl) { + world.stepBack(); + step_back_ctrl = false; + } + + if (apply_ctrl) { + bool resize_needed = false; + config_json["fps"] = fps_ctrl; + if (config_json["screen_width"].get() != width_ctrl || + config_json["screen_height"].get() != height_ctrl) { + config_json["screen_width"] = width_ctrl; + config_json["screen_height"] = height_ctrl; + rlImGuiShutdown(); + SetWindowSize(width_ctrl, height_ctrl); + rlImGuiSetup(dark_theme_ctrl); + resize_needed = true; + } + if (cell_size_ctrl != config_json["cell_size"].get() || + resize_needed) { + world.resize(config_json["screen_width"].get() / cell_size_ctrl, + config_json["screen_height"].get() / cell_size_ctrl); + render.updateCellSize(cell_size_ctrl); + rules.newWorld(&world); + config_json["cell_size"] = cell_size_ctrl; + } + if (dark_theme_ctrl != config_json["dark_theme"]) { + rlImGuiShutdown(); + rlImGuiSetup(dark_theme_ctrl); + config_json["dark_theme"] = dark_theme_ctrl; + } + apply_ctrl = false; + settings_window = false; + } + + // Accumulate time and update simulation at the adjusted speed + deltaTimeAccumulator += deltaTime; + + if (deltaTimeAccumulator >= timePerUpdate) { + // Reset accumulator + deltaTimeAccumulator -= timePerUpdate; + if (menu_state == MenuState::PLAY) { + world.saveCompressed(); + rules.update(); + } + } + BeginDrawing(); + ClearBackground(BLACK); + render.display(&world); + if (selecting) { + auto grid_mouse = snapToGrid(mousePos, cell_size_ctrl); + bool sel_x_less = (sel_pos.x < grid_mouse.x); + bool sel_y_less = (sel_pos.y < grid_mouse.y); + DrawRectangleLines(((sel_x_less)? sel_pos.x: grid_mouse.x), ((sel_y_less)? sel_pos.y: grid_mouse.y), + ((sel_x_less)? grid_mouse.x - sel_pos.x : sel_pos.x - grid_mouse.x), + ((sel_y_less)? grid_mouse.y - sel_pos.y : sel_pos.y - grid_mouse.y), + RED); + } + // Start ImGui frame + rlImGuiBegin(); + + bool control_panel_open = true; + ImGuiWindowFlags control_panel_flags = ImGuiWindowFlags_AlwaysAutoResize | + ImGuiWindowFlags_NoResize; + ImVec2 menu_pos(3, 3); + ImGui::SetNextWindowPos(menu_pos, ImGuiCond_Always); + ImGui::Begin("Control Panel", &control_panel_open, control_panel_flags); + ImGui::Checkbox("Play", &play_ctrl); + if (ImGui::Button("Step")) { + step_ctrl = true; + } + if (ImGui::Button("Step Back")) { + step_back_ctrl = true; + } + ImGui::Checkbox("Edit", &edit_ctrl); + ImGui::Checkbox("Clear", &clear_ctrl); + ImGui::Checkbox("Randomize", &rand_ctrl); + if (ImGui::Button((settings_window) ? "Hide settings" : "Show settings")) { + settings_window = !settings_window; + } + ImGui::Text("Generation: %zu", world.getCycle()); + ImGui::End(); + + if (settings_window) { + ImGuiWindowFlags settings_flags = + ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoResize; + ImGui::Begin("Settings", &settings_window, settings_flags); + ImGui::SliderInt("Window Width", &width_ctrl, 800, 1920); + ImGui::SliderInt("Window Height", &height_ctrl, 600, 1200); + ImGui::SliderInt("FPS", &fps_ctrl, 0, 30); + ImGui::SliderInt("Cell Size", &cell_size_ctrl, 4, 100); + ImGui::Checkbox("Dark Theme", &dark_theme_ctrl); + if (ImGui::Button("Save & Apply")) { + apply_ctrl = true; + } + ImGui::End(); + } + if (sel_ctrl) { + BeginTextureMode(selectionTexture); + ClearBackground(BLACK); + auto max_size = (sel_data[0] > sel_data[1]) ? sel_data[0] : sel_data[1]; + int fitted_width = 200 / max_size; + auto sel_it = sel_data.begin(); + sel_it += 2; // skip dimensions + for (int j = 0; j < sel_data[1]; j++) { + for (int i = 0; i < sel_data[0]; i++) { + if (*sel_it == 1) { + DrawRectangle(i * fitted_width, (sel_data[1] -j-1) * fitted_width, fitted_width, fitted_width, WHITE); + } + sel_it++; + } + } + EndTextureMode(); + ImGuiWindowFlags settings_flags = + ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoResize; + ImGui::Begin("Selection", &sel_ctrl, settings_flags); + rlImGuiImageSize(&selectionTexture.texture, 200, 200); + ImGui::InputText(sel_txt_input_hint.c_str(), patern_name, 255); + if (ImGui::Button("Save")) { + char path_buf[1024]; + ssize_t len = readlink("/proc/self/exe", path_buf, sizeof(path_buf)-1); + if (len != -1) { + // Create paterns dir if not present + std::filesystem::path paterns_dir = std::filesystem::path(path_buf).parent_path() / "paterns"; + if (!std::filesystem::exists(paterns_dir) && !std::filesystem::create_directory(paterns_dir)) { + std::cerr << "Failed to create paterns directory" << std::endl; + } else { // Could be optimized by early returning in a function + std::ofstream patern_file; + paterns_dir += '/'; + paterns_dir += patern_name; + patern_file.open(paterns_dir); + if (!patern_file) { + std::cerr << "Failed to create the patern file" << std::endl; + } else { + auto sel_it = sel_data.begin(); + sel_it += 2; // skip dimensions + patern_file << sel_data[0]; + patern_file << "|"; // Separator needed to split as ascii values + patern_file << sel_data[1]; + patern_file << "|"; // Separator needed to split as ascii values + for (int j = 0; j < sel_data[1]; j++) { + for (int i = 0; i < sel_data[0]; i++) { + patern_file << std::to_string(*sel_it); + if (*sel_it == 1) { + } + sel_it++; + } + } + patern_file << std::flush; + patern_file.close(); + } + } + sel_ctrl = false; + } + } + ImGui::SameLine(); + if (ImGui::Button("Discard")) { + sel_ctrl = false; + } + ImGui::End(); + } + // End ImGui frame + rlImGuiEnd(); + EndDrawing(); + } + config_file.open(config_file_name, std::ios::out | std::ios::trunc); + config_file << config_json.dump(2); + config_file.close(); + // Cleanup Selection texture + UnloadRenderTexture(selectionTexture); + rlImGuiShutdown(); + CloseWindow(); + return 0; +} diff --git a/src/render.cpp b/src/render.cpp new file mode 100644 index 0000000..f74fa19 --- /dev/null +++ b/src/render.cpp @@ -0,0 +1,82 @@ +/* ************************************************************************** */ +/* */ +/* / ) */ +/* render.cpp (\__/) ( ( */ +/* ) ( ) ) */ +/* By: lejulien ={ }= / / */ +/* ) `-------/ / */ +/* Created: 2023/01/09 12:44:30 by lejulien ( / */ +/* Updated: 2023/01/14 17:02:00 by lejulien \ | */ +/* */ +/* ************************************************************************** */ + +#include "../includes/render.hpp" + +#include "raylib.h" +#include + +// Constructor + +Render::Render(int cell_size) : cell_size_(cell_size) {} + +// Display function + +int HSVtoHex(float h) { + float r, g, b; + int i = int(h / 60) % 6; + float f = (h / 60) - i; + float p = 0.0f; + float q = 1.0f - f; + float t = f; + + switch (i) { + case 0: + r = 1.0f, g = t, b = p; + break; + case 1: + r = q, g = 1.0f, b = p; + break; + case 2: + r = p, g = 1.0f, b = t; + break; + case 3: + r = p, g = q, b = 1.0f; + break; + case 4: + r = t, g = p, b = 1.0f; + break; + default: + r = 1.0f, g = p, b = q; + break; + } + + int R = static_cast(r * 255); + int G = static_cast(g * 255); + int B = static_cast(b * 255); + + return (R << 24) | (G << 16) | (B << 8) | 255; // Return as 0xRRGGBB integer +} + +void Render::display_world(std::vector *data, int width, int height) { + float maxDist = + std::sqrt((width - 1) * (width - 1) + (height - 1) * (height - 1)); + for (int j = 0; j < height; j++) { + for (int i = 0; i < width; i++) { + if ((*data)[i + j * width]) { + float dist = std::sqrt(i * i + j * j); + float factor = dist / maxDist; + float hue = factor * 360.0f; // Map to 0-360 degrees + DrawRectangle(i * cell_size_, j * cell_size_, cell_size_, cell_size_, + GetColor(HSVtoHex(hue))); + } + } + } +} + +// Render loop + +void Render::display(World *world) { + display_world(world->getWorldData(), world->getWidth(), world->getHeight()); +} + +void Render::updateCellSize(int new_size) { cell_size_ = new_size; } diff --git a/src/rules.cpp b/src/rules.cpp new file mode 100644 index 0000000..e9c0298 --- /dev/null +++ b/src/rules.cpp @@ -0,0 +1,106 @@ +/* ************************************************************************** */ +/* */ +/* / ) */ +/* rules.cpp (\__/) ( ( */ +/* ) ( ) ) */ +/* By: lejulien ={ }= / / */ +/* ) `-------/ / */ +/* Created: 2023/01/09 12:18:36 by lejulien ( / */ +/* Updated: 2023/01/14 16:47:01 by lejulien \ | */ +/* */ +/* ************************************************************************** */ + +#include "../includes/rules.hpp" +#include "../includes/world.hpp" + +// Contructor + +Rules::Rules() {} + +// Private functions + +void Rules::offset_coord(int &i, int &j) { + if (i < 0) { + i = this->_width - 1; + } else if (i >= this->_width) { + i = 0; + } + if (j < 0) { + j = this->_height - 1; + } else if (j >= this->_height) { + j = 0; + } +} + +bool Rules::is_alive(int i, int j) { + offset_coord(i, j); + if (this->_buffer[i + j * this->_width]) + return true; + return false; +} + +void Rules::ortho_neighbors(int &neighbors, int i, int j) { + // top + if (is_alive(i, j - 1)) + neighbors++; + // bottom + if (is_alive(i, j + 1)) + neighbors++; + // left + if (is_alive(i - 1, j)) + neighbors++; + // right + if (is_alive(i + 1, j)) + neighbors++; +} + +void Rules::diag_neighbors(int &neighbors, int i, int j) { + // top-left + if (is_alive(i - 1, j - 1)) + neighbors++; + // top-right + if (is_alive(i + 1, j - 1)) + neighbors++; + // bottom-left + if (is_alive(i - 1, j + 1)) + neighbors++; + // bottom-right + if (is_alive(i + 1, j + 1)) + neighbors++; +} + +// Member function + +void Rules::setup(World *world) { + _world = world; + _width = world->getWidth(); + _height = world->getHeight(); +} + +void Rules::newWorld(World *world) { + _world = world; + _width = world->getWidth(); + _height = world->getHeight(); +} + +void Rules::update() { + int neighbors = 0; + std::vector *world_data = _world->getWorldData(); + this->_buffer = *world_data; + + // apply the rules for each cells + for (int j = 0; j < this->_height; j++) { + for (int i = 0; i < this->_width; i++) { + // count neighbors + neighbors = 0; + ortho_neighbors(neighbors, i, j); + diag_neighbors(neighbors, i, j); + + // Apply the rules + if (neighbors < 2 || neighbors > 3) + (*world_data)[i + j * this->_width] = false; + else if (neighbors == 3) + (*world_data)[i + j * this->_width] = true; + } + } +} diff --git a/src/world.cpp b/src/world.cpp new file mode 100644 index 0000000..3752280 --- /dev/null +++ b/src/world.cpp @@ -0,0 +1,155 @@ +/* ************************************************************************** */ +/* */ +/* / ) */ +/* world.cpp (\__/) ( ( */ +/* ) ( ) ) */ +/* By: lejulien ={ }= / / */ +/* ) `-------/ / */ +/* Created: 2023/01/09 12:25:55 by lejulien ( / */ +/* Updated: 2023/01/14 17:06:54 by lejulien \ | */ +/* */ +/* ************************************************************************** */ + +#include "../includes/world.hpp" + +#include + +// Constructor and destructor + +World::World(int width, int height) : _width(width), _height(height) { + // create world data + this->_data = new std::vector(width * height, false); +} + +World::~World() { delete this->_data; } + +// Getters + +std::vector *World::getWorldData() { return this->_data; } + +int World::getWidth() { return this->_width; } + +int World::getHeight() { return this->_height; } + +// Member function + +void World::randomize() { + // setup randomization + _history.clear(); + _cycle_index = 0; + auto now = std::chrono::high_resolution_clock::now(); + auto ms = std::chrono::duration_cast( + now.time_since_epoch()) + .count(); + + std::srand(ms); + + // filling data with random states + for (auto cell : *this->_data) { + if (std::rand() % 2) + cell = true; + else + cell = false; + } +} + +std::vector World::getCompressed() { + if (!_data || _data->empty()) { + return {}; + } + bool previous_state = (*_data)[0]; + std::vector compressed_out; + compressed_out.push_back(_width); + compressed_out.push_back(_height); + compressed_out.push_back((previous_state) ? 1 : 0); + unsigned int count = 0; + for (auto state : *_data) { + if (state != previous_state) { + compressed_out.push_back(count); + previous_state = state; + count = 1; + continue; + } + count++; + } + compressed_out.push_back(count); + return compressed_out; +} + +void World::loadCompressed(const std::vector &compressed) { + if (compressed.empty()) { + std::cerr << "Error: Compressed data is empty." << std::endl; + return; + } + auto it = compressed.begin(); + int width = *it++; + int height = *it++; + bool actual_state = (*it++ == 1); + auto new_data = new std::vector(); + while (it != compressed.end()) { + unsigned int count = *it++; + for (unsigned int i = 0; i < count; ++i) { + new_data->push_back(actual_state); + } + actual_state = !actual_state; + } + delete this->_data; + this->_data = new_data; + this->_width = width; + this->_height = height; +} + +void World::saveCompressed() { + _cycle_index++; + if (_history.size() >= MAX_HISTORY_SIZE) { + _history.erase(_history.begin()); + } + _history.push_back(getCompressed()); +} + +void World::clear() { + _history.clear(); + _cycle_index = 0; + for (auto cell : *this->_data) { + cell = false; + } +} + +void World::stepBack() { + if (_history.empty()) { + std::cerr << "Error: Invalid history index." << std::endl; + // [TODO] Generate new history from initial state to _history_index; + return; + } + _cycle_index--; + loadCompressed(_history[_history.size() - 1]); + _history.resize(_history.size() - 1); +} + +void World::setCell(int x, int y) { + _history.clear(); + _cycle_index = 0; + if (!_data) { + return; + } + (*_data)[x + y * _width] = !(*_data)[x + y * _width]; +} + +void World::resize(int width, int height) { + _width = width; + _height = height; + _data->resize(width * height, false); + clear(); +} + +std::vector World::getSelection(Vector2i &origin, Vector2i &size) { + // We assume the selection is in the grid for now + std::vector data = {static_cast(size.x), + static_cast(size.y)}; + for (int y = origin.y; y < origin.y + size.y; y++) { + for (int x = origin.x; x < origin.x + size.x; x++) { + data.push_back((*_data)[x + y * _width]); + } + } + return data; +}