feat: converted project to template

This commit is contained in:
Sara Gerretsen 2025-09-24 22:01:55 +02:00
parent 0f80dc8ca6
commit a3396bb6af
8 changed files with 48 additions and 590 deletions

View file

@ -1,5 +1,5 @@
cmake_minimum_required(VERSION 3.21)
project(GameOfLife)
project(CHANGEME)
set(CMAKE_BINARY_DIR "${CMAKE_SOURCE_DIR}/bin")
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}")
@ -15,11 +15,11 @@ add_subdirectory(vendor/SDL3/ EXCLUDE_FROM_ALL)
set(SDLTTF_VENDORED ON)
add_subdirectory(vendor/SDL3_ttf/ EXCLUDE_FROM_ALL)
add_executable(GameOfLife ${source_files})
target_link_libraries(GameOfLife PRIVATE SDL3_ttf::SDL3_ttf SDL3::SDL3)
add_executable(CHANGEME ${source_files})
target_link_libraries(CHANGEME PRIVATE SDL3_ttf::SDL3_ttf SDL3::SDL3)
add_custom_target(copy_assets
COMMAND ${CMAKE_COMMAND} -E
copy_directory ${CMAKE_SOURCE_DIR}/assets/ ${CMAKE_BINARY_DIR}/assets
)
add_dependencies(GameOfLife copy_assets)
add_dependencies(CHANGEME copy_assets)

View file

@ -1,16 +1,26 @@
# Conway's Game of Life SDL3 implementation
# Clay SDL3 Template
## Using Template
### Just use [`just`](https://just.systems/man/en/)
Run `just set-project-name {project name}` first.
And remember to `git remote set-url origin {repo}`.
### Otherwise
Modify the CMakeLists.txt manually, just replace CHANGEME with whatever your binary should be called.
## Compiling
> Remember to `git submodule update --init --recursive`!
### Just use [`just`](https://just.systems/man/en/)
### Just again
Use the justfile if you have `just` and `bear` installed:
`just configure` runs cmake configuration and generates a `compile_commands.json`.
`just configure` runs cmake configuration.
`just build` runs the cmake build and generates a `compile_commands.json` (assuming you have `bear` installed).
`just build` runs the cmake build.
### CMake works too, I guess
@ -22,10 +32,6 @@ Same as always
## Files
### simulation.h/cpp
The Conway's Game of Life generation ticking and rendering.
### thread_pool.h/cpp
Thread pool used for parallel GoL generation ticking.
@ -54,13 +60,12 @@ Handling of input events.
Entrypoint, setup, and main application loop.
## Prerequisites
* Compiler capable of C++23 and C23.
* CMake 3.21 or higher
* Compiler capable of C++23 and C23.
## Dependencies
### SDL3
@ -79,9 +84,11 @@ Included as files in `vendor/clay/clay.h`; Single header library.
Included as files in `vendor/renderer/` and compiled as part of the project.
> Note: Mildly modified from the official Clay_SDL3_renderer to enable more advanced styling.
## Code Standards
* Keep program structure as simple as possible. No `Application` class or other Java-isms. Prefer namespaces with static lifetime variables.
* Keep program structure as simple as possible. No `class Application {` class or other Java-isms. Prefer namespaces with static lifetime variables.
* Use STL where possible. Don't reinvent the wheel.
@ -93,12 +100,16 @@ Included as files in `vendor/renderer/` and compiled as part of the project.
* const applies to the name to it's left, so it goes after the type, not `const int x;` but `int const x`.
* \* and & flush with the declaration name. `void Function(Object const &inRef)`
* \* and & flush with the declaration name. `Type const &Function(Type &inRef)`
> (1) Bracket exceptions:
> * using scoped_lock in arbitrary blocks, prefer tailed lisp brackets
```
void MyFunction() { // K&R here
struct Data {
int x{ 0 }, y{ 0 };
};
void MyFunction(Data const &data) { // K&R here
DoAsynchronousThings();
{ scoped_lock lock{ myMutex };
myVariable++;

View file

@ -1,14 +1,19 @@
build:
# BUILDING
bear -- cmake --build build
cmake --build build
run:
cd bin/ && DiceGui
cd bin/ && CHANGEME
configure:
# CONFIGURING WITH PREMAKE
cmake -S. -Bbuild
cmake -S. -Bbuild -DCMAKE_EXPORT_COMPILE_COMMANDS=ON
clean:
# CLEANING BUILD ARTEFACTS
rm -r bin/**
rm -r build/**
set-project-name projectname: clean
git remote set-url origin ""
sed -i "s/CHANGEME/{{projectname}}/g" ./CMakeLists.txt ./justfile

View file

@ -1,173 +1,24 @@
#include "application.h"
#include "style.h"
#include "elements.h"
#include "simulation.h"
#include <SDL3/SDL.h>
#include <clay/clay.h>
namespace application {
static bool isSimulating{ false };
static bool lockFramerate{ true };
static void SetSimulatingButton(Clay_ElementId element, Clay_PointerData pointer, intptr_t data) {
if (pointer.state == CLAY_POINTER_DATA_PRESSED_THIS_FRAME) {
isSimulating = data == 1;
}
}
static void RandomizeFieldButton(Clay_ElementId element, Clay_PointerData pointer, intptr_t data) {
if (pointer.state == CLAY_POINTER_DATA_PRESSED_THIS_FRAME) {
simulation::InitializeRandom(2, 300);
}
}
static void StepSimulationButton(Clay_ElementId element, Clay_PointerData pointer, intptr_t data) {
if (pointer.state == CLAY_POINTER_DATA_PRESSED_THIS_FRAME) {
simulation::Step();
}
}
static void StepControl() {
CLAY_AUTO_ID(style::PanelContainer(1, {
.layout = {
.sizing = { CLAY_SIZING_GROW(), CLAY_SIZING_FIT() },
.childGap = 16,
.layoutDirection = CLAY_TOP_TO_BOTTOM,
},
})) {
CLAY_AUTO_ID({
.layout = {
.childGap = 16
}
}) {
if (isSimulating) {
elements::TextButton(CLAY_STRING("Pause"), style::warningButton, &SetSimulatingButton, false);
} else {
elements::TextButton(CLAY_STRING("Start"), style::proceedButton, &SetSimulatingButton, true);
elements::TextButton(CLAY_STRING("Step"), style::actionButton, &StepSimulationButton);
}
}
elements::Toggle(CLAY_STRING("Threaded"), style::actionButton, simulation::threadedDesired);
elements::Toggle(CLAY_STRING("Show Changes"), style::actionButton, simulation::drawDebugInfo);
elements::Toggle(CLAY_STRING("Lock Speed"), style::actionButton, lockFramerate);
}
}
static void DebugInfoLegend() {
Clay_ElementDeclaration style{style::PanelContainer(1, {
.layout = {
.padding = {
.left = 20, .right = 0, .top = 0, .bottom = 0
},
.childGap = 10,
.layoutDirection = CLAY_TOP_TO_BOTTOM,
}
})
};
style.cornerRadius = { 0, style::defaultRadius, 0, style::defaultRadius };
CLAY_AUTO_ID(style) {
CLAY_AUTO_ID({
.layout = {
.childGap = 10,
},
}) {
CLAY_AUTO_ID({
.layout = {
.sizing = { CLAY_SIZING_FIXED(20), CLAY_SIZING_FIXED(20) }
},
.border = { {255, 0, 255, 255 }, CLAY_BORDER_OUTSIDE(3) }
static void SampleHeader() {
elements::Header(CLAY_STRING("Left Panel"), 2, {
.textColor = style::color::white
});
elements::Body(CLAY_STRING("Underpopulated"), {
.textColor = style::TextColor(0)
});
}
CLAY_AUTO_ID({
.layout = {
.childGap = 10,
}
}) {
CLAY_AUTO_ID({
.layout = {
.sizing = { CLAY_SIZING_FIXED(20), CLAY_SIZING_FIXED(20) }
},
.border = { {255, 0, 0, 255 }, CLAY_BORDER_OUTSIDE(3) }
});
elements::Body(CLAY_STRING("Overpopulated"), {
.textColor = style::TextColor(0)
});
}
CLAY_AUTO_ID({
.layout = {
.childGap = 10,
}
}) {
CLAY_AUTO_ID({
.layout = {
.sizing = { CLAY_SIZING_FIXED(20), CLAY_SIZING_FIXED(20) }
},
.border = { {0, 255, 0, 255 }, CLAY_BORDER_OUTSIDE(3) }
});
elements::Body(CLAY_STRING("Born"), {
.textColor = style::TextColor(0)
});
}
elements::Body(CLAY_STRING("Warning:\nDrawing simulation changes\ngreatly lowers performance."), {
.textColor = style::TextColor(2)
});
}
}
static void PrimaryControls() {
static bool isOpen{ true };
if (isOpen) {
CLAY_AUTO_ID(style::LeftPanelContainer(0, {
.layout = {
.sizing = { CLAY_SIZING_FIT(.18) },
}
})) {
CLAY_AUTO_ID({
.layout = {
.childGap = 16,
.layoutDirection = CLAY_TOP_TO_BOTTOM,
},
.clip = {
true, true, Clay_GetScrollOffset()
}
}) {
StepControl();
if (!isSimulating) {
CLAY_AUTO_ID(style::LeftPanelContainer(1, {
.layout = { .sizing = { CLAY_SIZING_GROW(), CLAY_SIZING_FIT() } }
})) {
elements::TextButton(CLAY_STRING("Randomize"), style::actionButton, &RandomizeFieldButton);
}
}
}
}
}
if (simulation::drawDebugInfo) {
DebugInfoLegend();
}
}
static void TryStep() {
static uint64_t lastStep = 0;
if (isSimulating) {
double deltaTime{ (double)(SDL_GetTicks() - lastStep) * 0.001 };
constexpr double targetDeltaTime = 1.0/24.0;
if (!lockFramerate || deltaTime > targetDeltaTime) {
lastStep = SDL_GetTicks();
simulation::Step();
}
}
}
Clay_RenderCommandArray RenderApplication() {
TryStep();
Clay_BeginLayout();
CLAY(CLAY_ID("OuterContainer"), style::Window()) {
simulation::SetSimulationHovered(Clay_Hovered());
PrimaryControls();
CLAY_AUTO_ID(style::PanelContainer(0, {
.layout = { .sizing = { CLAY_SIZING_PERCENT(0.15), CLAY_SIZING_GROW() } }
})) {
SampleHeader();
}
}
return Clay_EndLayout();
}

View file

@ -5,7 +5,6 @@
namespace elements {
typedef void(*OnHoveredFn)(Clay_ElementId element, Clay_PointerData pointer, intptr_t data);
void TextButton(Clay_String text, Clay_Color color, OnHoveredFn onHovered, intptr_t onHoveredData = 0);
void Toggle(Clay_String label, Clay_Color selected, bool &state);
void Body(Clay_String string, Clay_TextElementConfig baseCfg = {});

View file

@ -3,7 +3,6 @@
#include "application.h"
#include "input.h"
#include "resources.h"
#include "simulation.h"
#include <SDL3/SDL.h>
#include <SDL3/SDL_error.h>
#include <SDL3/SDL_events.h>
@ -133,7 +132,6 @@ int main(int argc, char *argv[]) {
Clay_UpdateScrollContainers(true, input::scrollMotion, deltaTime);
SDL_SetRenderDrawColor(renderer, 0, 0, 0, 255);
SDL_RenderClear(renderer);
simulation::Draw(renderer, 0.01);
Clay_RenderCommandArray commands{ application::RenderApplication() };
SDL_Clay_RenderClayCommands(&backendData, &commands);
SDL_RenderPresent(renderer);

View file

@ -1,356 +0,0 @@
#include "simulation.h"
#include "SDL3/SDL_timer.h"
#include "input.h"
#include "thread_pool.h"
#include <SDL3/SDL_log.h>
#include <SDL3/SDL_render.h>
#include <algorithm>
#include <clay/clay.h>
#include <cstdint>
#include <mutex>
#include <set>
#include <vector>
#ifdef __glibc_likely
#define likely(cond_) __glibc_likely(cond_)
#else
#define likely(cond_) (cond_)
#endif
#define SIM_MULTITHREADING 1
namespace simulation {
// Encapsulates the data relevant to a GoL rule's workload
struct ThreadWorkload {
size_t seg_idx{0}, seg_len{1};
std::mutex mtx{};
std::vector<Cell> changes{};
};
bool drawDebugInfo{ true }; // draw next step's changes over current state
bool threadedDesired{ true }; // multithread generation steps
static bool actuallyThreaded{ false }; // notes whether or not currently ongoing tasks are multithreaded
size_t threadsPerTask{ std::thread::hardware_concurrency() };
static std::set<Cell> living{}; // The set of currently alive cells
// Ruleset workload abstraction
static std::vector<std::shared_ptr<ThreadWorkload>> overpopulated{};
static std::vector<std::shared_ptr<ThreadWorkload>> underpopulated{};
static std::vector<std::shared_ptr<ThreadWorkload>> born{};
// Multithreading
static unsigned tasks{ 0 };
static std::mutex tasksMutex{};
static std::condition_variable tasksChanged{};
// ns timestamp of the start of the last generation change calculation
static uint64_t generationStartTime{ 0 };
CellIterator::CellIterator(Cell begin, Cell end)
: state{ begin } , begin{ begin }, end{ end } {}
CellIterator::CellIterator(Cell begin, Cell end, Cell state)
: state{ state } , begin{ begin }, end{ end } {}
static inline bool CellIsAlive(Cell const &cell) {
return living.contains(cell);
}
CellIterator &CellIterator::operator++() {
++(this->state.x);
if (this->state.x == this->end.x) {
this->state.x = this->begin.x;
this->state.y = SDL_min(this->state.y + 1, this->end.y);
}
return *this;
}
bool CellIterator::operator==(CellIterator const &src) const {
return src.begin == this->begin && src.end == this->end && (src.state == this->state || src.at_end() == this->at_end());
}
bool CellIterator::operator!=(CellIterator const &src) const {
return src.begin != this->begin || src.end != this->end || (src.state != this->state && src.at_end() != this->at_end());
}
Cell const &CellIterator::operator*() const {
return state;
}
bool CellIterator::at_end() const {
return this->state.y == this->end.y;
}
static size_t CountNeighbors(Cell const &cell) {
size_t count{ 0 };
for (Cell const &c : CellRange{ {cell.x - 1, cell.y - 1}, { cell.x + 2, cell.y + 2} }) {
if (c != cell && CellIsAlive(c)) {
++count;
}
}
return count;
}
// Block the thread until all tasks are done
// after this function, all code in this file is synchronous, until another thread is started
static void BlockUntilTasksDone() {
if (actuallyThreaded) {
std::unique_lock lock{ tasksMutex };
while (tasks > 0) {
tasksChanged.wait(lock);
}
} else while (tasks > 0) { }
}
static void TaskBegin() {
std::scoped_lock lock{ tasksMutex };
tasks++;
}
static void TaskComplete() {
if (actuallyThreaded) {
std::scoped_lock lock{ tasksMutex };
if (--tasks == 0) {
SDL_Log("Generation Complete %lfs", (double)(SDL_GetTicksNS() - generationStartTime) * 0.000000001);
}
tasksChanged.notify_all();
} else if (tasks == 3) {
SDL_Log("Generation complete %lfs", (double)(SDL_GetTicksNS() - generationStartTime) * 0.000000001);
tasks = 0;
}
}
static void FindOverpopulated(std::shared_ptr<ThreadWorkload> wl) {
TaskBegin();
std::scoped_lock lock{ wl->mtx };
wl->changes.clear();
std::set<Cell>::iterator begin{ living.begin() };
std::set<Cell>::iterator end{ living.begin() };
std::advance(begin, wl->seg_idx * wl->seg_len);
std::advance(end, wl->seg_idx * wl->seg_len + wl->seg_len);
std::copy_if(begin, end, std::back_inserter(wl->changes),
[&](Cell const &c) -> bool {
return CountNeighbors(c) > 3;
});
TaskComplete();
}
static void FindUnderpopulated(std::shared_ptr<ThreadWorkload> wl) {
TaskBegin();
std::scoped_lock lock{ wl->mtx };
wl->changes.clear();
std::set<Cell>::iterator begin{ living.begin() };
std::set<Cell>::iterator end{ living.begin() };
std::advance(begin, wl->seg_idx * wl->seg_len);
std::advance(end, wl->seg_idx * wl->seg_len + wl->seg_len);
std::copy_if(begin, end, std::back_inserter(wl->changes),
[&](Cell const &c) -> bool {
return CountNeighbors(c) < 2;
});
TaskComplete();
}
static void FindBorn(std::shared_ptr<ThreadWorkload> wl) {
TaskBegin();
std::scoped_lock lock{ wl->mtx };
wl->changes.clear();
std::set<Cell>::iterator itr{ living.begin() };
std::advance(itr, wl->seg_idx * wl->seg_len);
std::for_each_n(itr, wl->seg_len, [&wl](Cell const &center){
CellRange range{{ center.x - 1, center.y - 1 }, {center.x + 2, center.y + 2 }};
std::copy_if(range.begin(), range.end(), std::back_inserter(wl->changes),
[&](Cell const &c) -> bool {
return !CellIsAlive(c) && CountNeighbors(c) == 3;
});
});
TaskComplete();
}
static inline void TickGenerationThreaded() {
SDL_Log("Multithreading ON");
generationStartTime = SDL_GetTicksNS();
size_t const seg_length{ living.size() / threadsPerTask };
for (size_t i{ 0 }; i < threadsPerTask; ++i) {
if (overpopulated[i] == nullptr)
overpopulated[i] = std::make_shared<ThreadWorkload>();
{ std::scoped_lock lock{ overpopulated[i]->mtx };
overpopulated[i]->seg_idx = i; overpopulated[i]->seg_len = seg_length;
threading::tasks.ScheduleTask(std::bind(FindOverpopulated, overpopulated[i]));
}
if (underpopulated[i] == nullptr)
underpopulated[i] = std::make_shared<ThreadWorkload>();
{ std::scoped_lock lock{ underpopulated[i]->mtx };
underpopulated[i]->seg_idx = i; underpopulated[i]->seg_len = seg_length;
threading::tasks.ScheduleTask(std::bind(FindUnderpopulated, underpopulated[i]));
}
if (born[i] == nullptr)
born[i] = std::make_shared<ThreadWorkload>();
{ std::scoped_lock lock{ born[i]->mtx };
born[i]->seg_idx = i; born[i]->seg_len = seg_length;
threading::tasks.ScheduleTask(std::bind(FindBorn, born[i]));
}
}
}
static inline void TickGenerationSynchronous() {
SDL_Log("Multithreading OFF");
generationStartTime = SDL_GetTicksNS();
born[0]->seg_idx = underpopulated[0]->seg_idx = overpopulated[0]->seg_idx = 0;
born[0]->seg_len = underpopulated[0]->seg_len = overpopulated[0]->seg_len = living.size();
FindOverpopulated(overpopulated[0]);
FindUnderpopulated(underpopulated[0]);
FindBorn(born[0]);
}
static void PopulateChanges() {
static bool first_run{ true };
BlockUntilTasksDone();
// for some reason three tasks per thread is faster than one task per thread, i don't get it either
if (threadedDesired) {
if (first_run || !actuallyThreaded) {
actuallyThreaded = true;
first_run = false;
born.resize(threadsPerTask);
underpopulated.resize(threadsPerTask);
overpopulated.resize(threadsPerTask);
}
TickGenerationThreaded();
} else {
if (first_run || actuallyThreaded) {
actuallyThreaded = false;
first_run = false;
born.resize(1);
underpopulated.resize(1);
overpopulated.resize(1);
}
TickGenerationSynchronous();
}
}
void InitializeRandom(size_t livingChance, int64_t fillArea) {
BlockUntilTasksDone();
living.clear();
Cell itr{ 0, 0 };
while (itr.y < fillArea) {
if (std::rand() % livingChance == 0 ) {
living.insert(itr);
}
++itr.x;
if (itr.x == fillArea) {
itr.x = 0;
++itr.y;
}
}
PopulateChanges();
}
void Step() {
BlockUntilTasksDone();
for (std::shared_ptr<ThreadWorkload> wl : underpopulated) {
if (wl == nullptr) continue;
std::scoped_lock l{ wl->mtx };
for (Cell const &cell : wl->changes)
living.erase(cell);
}
for (std::shared_ptr<ThreadWorkload> wl : overpopulated) {
if (wl == nullptr) continue;
std::scoped_lock l{ wl->mtx };
for (Cell const &cell : wl->changes)
living.erase(cell);
}
for (std::shared_ptr<ThreadWorkload> wl : born) {
if (wl == nullptr) continue;
std::scoped_lock l{ wl->mtx };
for (Cell const &cell : wl->changes)
living.insert(cell);
}
PopulateChanges();
}
static bool simulationHovered{ false };
static SDL_FPoint viewOffset{ 0, 0 };
void Draw(SDL_Renderer *renderer, double cellSizePercent) {
if (simulationHovered) {
viewOffset.x += input::scrollMotion.x;
viewOffset.y += input::scrollMotion.y;
}
int w, h;
SDL_GetCurrentRenderOutputSize(renderer, &w, &h);
float const cellWidth = static_cast<float>(w) * cellSizePercent;
SDL_FRect cellRect{
0, 0, cellWidth, cellWidth
};
SDL_SetRenderDrawColor(renderer, 255, 255, 255, 255);
for (Cell const &cell : living) {
cellRect.x = (viewOffset.x + cell.x) * cellRect.w;
cellRect.y = (viewOffset.y + cell.y) * cellRect.h;
if (cellRect.x < -cellRect.w || cellRect.y < -cellRect.h || cellRect.x > w || cellRect.y > h)
continue;
SDL_RenderFillRect(renderer, &cellRect);
}
if (!drawDebugInfo) {
return;
}
BlockUntilTasksDone();
for (std::shared_ptr<ThreadWorkload> wl : overpopulated) {
std::scoped_lock l{ wl->mtx };
SDL_SetRenderDrawColor(renderer, 255, 0, 0, 255);
for (Cell const &cell : wl->changes) {
cellRect.x = (viewOffset.x + cell.x) * cellRect.w;
cellRect.y = (viewOffset.y + cell.y) * cellRect.h;
if (cellRect.x < -cellRect.w || cellRect.y < -cellRect.h || cellRect.x > w || cellRect.y > h)
continue;
SDL_RenderRect(renderer, &cellRect);
}
}
for (std::shared_ptr<ThreadWorkload> wl : underpopulated) {
std::scoped_lock l{ wl->mtx };
SDL_SetRenderDrawColor(renderer, 255, 0, 255, 255);
for (Cell const &cell : wl->changes) {
cellRect.x = (viewOffset.x + cell.x) * cellRect.w;
cellRect.y = (viewOffset.y + cell.y) * cellRect.h;
if (cellRect.x < -cellRect.w || cellRect.y < -cellRect.h || cellRect.x > w || cellRect.y > h)
continue;
SDL_RenderRect(renderer, &cellRect);
}
}
for (std::shared_ptr<ThreadWorkload> wl : born) {
std::scoped_lock l{ wl->mtx };
SDL_SetRenderDrawColor(renderer, 0, 255, 0, 255);
for (Cell const &cell : wl->changes) {
cellRect.x = (viewOffset.x + cell.x) * cellRect.w;
cellRect.y = (viewOffset.y + cell.y) * cellRect.h;
if (cellRect.x < -cellRect.w || cellRect.y < -cellRect.h || cellRect.x > w || cellRect.y > h)
continue;
SDL_RenderRect(renderer, &cellRect);
}
}
}
void SetSimulationHovered(bool value) {
simulationHovered = value;
}
bool operator==(Cell const &lhs, Cell const &rhs) {
return lhs.x == rhs.x && lhs.y == rhs.y;
}
bool operator!=(Cell const &lhs, Cell const &rhs) {
return lhs.x != rhs.x || lhs.y != rhs.y;
}
bool operator<(Cell const &lhs, Cell const &rhs) {
if (lhs.y == rhs.y) return lhs.x < rhs.x;
else return lhs.y < rhs.y;
}
}

View file

@ -1,50 +0,0 @@
#ifndef SIMULATION_H
#define SIMULATION_H
#include <SDL3/SDL_rect.h>
#include <SDL3/SDL_render.h>
#include <clay/clay.h>
namespace simulation {
struct Cell {
int64_t x{ 0 }, y{ 0 };
};
class CellIterator {
friend class CellRange;
public: CellIterator(Cell begin, Cell end);
CellIterator &operator++();
CellIterator &operator=(CellIterator const &src);
bool operator==(CellIterator const &src) const;
bool operator!=(CellIterator const &src) const;
Cell const &operator*() const;
bool at_end() const;
private: CellIterator(Cell begin, Cell end, Cell state);
Cell state{ 0, 0 };
Cell begin{ 0, 0 }, end{ 0, 0 };
};
class CellRange {
private:
Cell beginCell{ 0, 0 }, endCell{ 0, 0 };
public: CellRange(Cell begin, Cell end) : beginCell{ begin }, endCell{ end } {}
CellIterator begin() { return CellIterator{ beginCell, endCell }; }
CellIterator end() { return CellIterator{ beginCell, endCell, endCell }; }
};
extern bool drawDebugInfo;
extern bool threadedDesired;
extern size_t threadsPerTask;
void InitializeRandom(size_t livingChance, int64_t fillArea);
void Step();
void Draw(SDL_Renderer *renderer, double cellSizePercent);
void SetSimulationHovered(bool value);
bool operator==(Cell const &lhs, Cell const &rhs);
bool operator!=(Cell const &lhs, Cell const &rhs);
bool operator<(Cell const &lhs, Cell const &rhs);
}
#endif // !SIMULATION_H