Initial commit

This commit is contained in:
shylie 2026-04-18 11:00:58 -04:00
commit 8193df09e0
7 changed files with 598 additions and 0 deletions

12
.clang-format Normal file
View File

@ -0,0 +1,12 @@
BasedOnStyle: GNU
SpaceBeforeParens: ControlStatementsExceptControlMacros
PointerAlignment: Left
UseTab: Never
IndentWidth: 2
ContinuationIndentWidth: 2
ConstructorInitializerIndentWidth: 2
BreakAfterReturnType: Automatic
BreakConstructorInitializers: AfterColon
PackConstructorInitializers: Never
IncludeBlocks: Regroup
BreakBeforeBraces: Allman

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
build/

32
CMakeLists.txt Normal file
View File

@ -0,0 +1,32 @@
cmake_minimum_required(VERSION 3.20)
project(sand)
find_package(PkgConfig REQUIRED)
pkg_check_modules(LUAJIT REQUIRED luajit)
add_library(sand SHARED
src/sand.cpp
src/lualib.cpp
)
target_include_directories(sand
PUBLIC include
PRIVATE ${LUAJIT_INCLUDE_DIRS}
)
target_link_libraries(sand PRIVATE ${LUAJIT_LIBRARIES})
set_target_properties(sand PROPERTIES
CXX_STANDARD 20
CXX_STANDARD_REQUIRED ON
)
find_package(SDL3 CONFIG REQUIRED)
add_executable(sandtest
src/main.cpp
)
target_link_libraries(sandtest PRIVATE sand SDL3)
set_target_properties(sandtest PROPERTIES
CXX_STANDARD 20
CXX_STANDARD_REQUIRED ON
)

114
include/sand.h Normal file
View File

@ -0,0 +1,114 @@
#ifndef SAND_H
#define SAND_H
#include <cstdint>
#include <string>
#include <unordered_map>
#include <vector>
namespace sand
{
struct type
{
uint16_t id;
uint16_t default_result;
};
using type_map = std::unordered_map<std::string, type>;
class RulesBuilder;
class TypeBuilder
{
friend RulesBuilder;
public:
TypeBuilder& add_type(const std::string& name);
TypeBuilder& add_type(const std::string& name,
const std::string& default_result);
RulesBuilder finish();
private:
uint16_t next_type_id = 0;
type_map types;
}; // class TypeBuilder
class Rule;
class Sand;
class RulesBuilder
{
friend TypeBuilder;
friend Rule;
friend Sand;
public:
Rule add_rule(const std::string& source_name, const std::string& to);
Sand build(uint16_t width, uint16_t height, const std::string& initial);
private:
RulesBuilder(const TypeBuilder& type_builder);
void finish_rule(const Rule& rule);
type_map types;
std::unordered_map<uint16_t,
std::vector<std::pair<unsigned __int128, uint16_t>>>
rules;
}; // class RulesBuilder
class Rule
{
friend RulesBuilder;
public:
using match_fn = bool (*)(const std::string& name);
~Rule();
Rule& top_left(match_fn match);
Rule& top_middle(match_fn match);
Rule& top_right(match_fn match);
Rule& middle_left(match_fn match);
Rule& middle_right(match_fn match);
Rule& bottom_left(match_fn match);
Rule& bottom_middle(match_fn match);
Rule& bottom_right(match_fn match);
private:
Rule(RulesBuilder& builder, uint16_t source, uint16_t to);
RulesBuilder& builder;
uint16_t source;
uint16_t to;
match_fn matches[8];
}; // class RuleBuilder
class Sand
{
friend RulesBuilder;
public:
void set(uint16_t x, uint16_t y, const std::string& type);
uint16_t get(uint16_t x, uint16_t y);
void update();
private:
Sand(const RulesBuilder& builder, uint16_t width, uint16_t height,
const std::string& initial);
uint16_t width;
uint16_t height;
type_map types;
std::unordered_map<uint16_t, uint16_t> default_conversions;
std::unordered_map<uint16_t,
std::vector<std::pair<unsigned __int128, uint16_t>>>
rules;
std::vector<uint16_t> elements;
bool iter;
}; // class Sand
} // namespace sand
#endif // SAND_H

12
src/lualib.cpp Normal file
View File

@ -0,0 +1,12 @@
#ifdef __cplusplus
extern "C"
{
#endif // __cplusplus
#include <luajit.h>
#ifdef __cplusplus
}
#endif // __cplusplus
extern "C" int luaopen_sand(lua_State* state) { return 1; }

83
src/main.cpp Normal file
View File

@ -0,0 +1,83 @@
#include <SDL3/SDL.h>
#include <SDL3/SDL_events.h>
#include <SDL3/SDL_mouse.h>
#include <SDL3/SDL_oldnames.h>
#include <SDL3/SDL_surface.h>
#include <SDL3/SDL_timer.h>
#include <SDL3/SDL_video.h>
#include <sand.h>
uint8_t lookup[][3]
= { { 0, 0, 0 }, { 50, 0, 0 }, { 100, 100, 100 }, { 200, 200, 200 } };
int main(int argc, char** argv)
{
auto rb = sand::TypeBuilder()
.add_type("offgrid")
.add_type("air")
.add_type("stone1")
.add_type("stone2")
.finish();
rb.add_rule("air", "stone1")
.top_middle([](const auto& t) { return t == "stone1"; });
rb.add_rule("stone1", "air")
.top_middle([](const auto& t) { return t == "air" || t == "offgrid"; })
.bottom_middle([](const auto& t) { return t == "air"; });
auto s = rb.build(64, 64, "air");
SDL_Init(SDL_INIT_VIDEO);
SDL_Window* window = SDL_CreateWindow("sand test", 512, 512, 0);
SDL_Surface* surface = SDL_CreateSurface(64, 64, SDL_PIXELFORMAT_RGB24);
int prev_ms = SDL_GetTicks();
float time = 0;
bool cont = true;
while (cont)
{
SDL_Event e;
while (SDL_PollEvent(&e))
{
if (e.type == SDL_EVENT_QUIT)
{
cont = false;
}
}
int ms = SDL_GetTicks();
float dt = (ms - prev_ms) / 1000.0f;
prev_ms = ms;
time += dt;
float x, y;
int state;
if (time > 0.008 && (state = SDL_GetMouseState(&x, &y)))
{
time = 0;
s.set(x * 64 / 512, y * 64 / 512, state == 1 ? "stone1" : "stone2");
}
SDL_LockSurface(surface);
for (int i = 0; i < 64; i++)
{
for (int j = 0; j < 64; j++)
{
memcpy(&static_cast<uint8_t*>(surface->pixels)[3 * (i + j * 64)],
lookup[s.get(i, j)], 3);
}
}
SDL_UnlockSurface(surface);
SDL_BlitSurfaceScaled(surface, nullptr, SDL_GetWindowSurface(window),
nullptr, SDL_SCALEMODE_NEAREST);
SDL_UpdateWindowSurface(window);
s.update();
}
SDL_DestroyWindow(window);
SDL_Quit();
return 0;
}

344
src/sand.cpp Normal file
View File

@ -0,0 +1,344 @@
#include "sand.h"
#include <algorithm>
#include <cstdint>
using namespace sand;
namespace
{
template <typename T>
T& binary_search(T element, T* elements, size_t begin, size_t end,
auto (*fn)(T, T)->bool)
{
if (end - begin == 1)
{
return elements[begin];
}
size_t index = begin + (end - begin) / 2;
if (!fn(element, elements[index]) && !fn(elements[index], element))
{
return elements[index];
}
else if (fn(element, elements[index]))
{
return binary_search<T>(element, elements, index, end, fn);
}
else
{
return binary_search<T>(element, elements, begin, index, fn);
}
}
} // namespace
TypeBuilder& TypeBuilder::add_type(const std::string& name)
{
if (!types.contains(name))
{
types[name] = { .id = next_type_id, .default_result = next_type_id };
next_type_id += 1;
}
return *this;
}
TypeBuilder& TypeBuilder::add_type(const std::string& name,
const std::string& default_result)
{
add_type(name);
add_type(default_result);
types[name].default_result = types[default_result].id;
return *this;
}
RulesBuilder TypeBuilder::finish() { return RulesBuilder(*this); }
Rule RulesBuilder::add_rule(const std::string& name, const std::string& to)
{
return Rule(*this, types[name].id, types[to].id);
}
Sand RulesBuilder::build(uint16_t width, uint16_t height,
const std::string& initial)
{
for (auto& [key, rule] : rules)
{
std::sort(rule.begin(), rule.end(),
[](auto a, auto b) { return a.first < b.first; });
}
return Sand(*this, width, height, initial);
}
RulesBuilder::RulesBuilder(const TypeBuilder& type_builder) :
types(type_builder.types)
{
}
void RulesBuilder::finish_rule(const Rule& rule)
{
// 8 nested loops is kinda silly, maybe there's a better way to do this?
// top left
for (const auto& [tl_name, tl_type] : types)
{
if (!rule.matches[0] || rule.matches[0](tl_name))
{
// top middle
for (const auto& [tm_name, tm_type] : types)
{
if (!rule.matches[1] || rule.matches[1](tm_name))
{
// top right
for (const auto& [tr_name, tr_type] : types)
{
if (!rule.matches[2] || rule.matches[2](tr_name))
{
// middle left
for (const auto& [ml_name, ml_type] : types)
{
if (!rule.matches[3] || rule.matches[3](ml_name))
{
// middle right
for (const auto& [mr_name, mr_type] : types)
{
if (!rule.matches[4] || rule.matches[4](mr_name))
{
// bottom left
for (const auto& [bl_name, bl_type] : types)
{
if (!rule.matches[5] || rule.matches[5](bl_name))
{
// bottom middle
for (const auto& [bm_name, bm_type] : types)
{
if (!rule.matches[6] || rule.matches[6](bm_name))
{
// bottom right
for (const auto& [br_name, br_type] : types)
{
if (!rule.matches[7]
|| rule.matches[7](br_name))
{
unsigned __int128 neighbors = 0;
neighbors += tl_type.id;
neighbors <<= 16;
neighbors += tm_type.id;
neighbors <<= 16;
neighbors += tr_type.id;
neighbors <<= 16;
neighbors += ml_type.id;
neighbors <<= 16;
neighbors += mr_type.id;
neighbors <<= 16;
neighbors += bl_type.id;
neighbors <<= 16;
neighbors += bm_type.id;
neighbors <<= 16;
neighbors += br_type.id;
if (!rules.contains(rule.source))
{
rules[rule.source] = {};
}
rules[rule.source].push_back(
{ neighbors, rule.to });
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
Rule::~Rule() { builder.finish_rule(*this); }
Rule& Rule::top_left(match_fn match)
{
matches[0] = match;
return *this;
}
Rule& Rule::top_middle(match_fn match)
{
matches[1] = match;
return *this;
}
Rule& Rule::top_right(match_fn match)
{
matches[2] = match;
return *this;
}
Rule& Rule::middle_left(match_fn match)
{
matches[3] = match;
return *this;
}
Rule& Rule::middle_right(match_fn match)
{
matches[4] = match;
return *this;
}
Rule& Rule::bottom_left(match_fn match)
{
matches[5] = match;
return *this;
}
Rule& Rule::bottom_middle(match_fn match)
{
matches[6] = match;
return *this;
}
Rule& Rule::bottom_right(match_fn match)
{
matches[7] = match;
return *this;
}
Rule::Rule(RulesBuilder& builder, uint16_t source, uint16_t to) :
builder(builder),
source(source),
to(to),
matches{}
{
}
Sand::Sand(const RulesBuilder& builder, uint16_t width, uint16_t height,
const std::string& initial) :
width(width),
height(height),
types(builder.types),
rules(builder.rules),
elements(width * height * 2, types[initial].id),
iter(false)
{
for (const auto& [name, type] : types)
{
default_conversions[type.id] = type.default_result;
}
}
void Sand::set(uint16_t x, uint16_t y, const std::string& type)
{
elements[x + y * width + iter * width * height] = types[type].id;
}
uint16_t Sand::get(uint16_t x, uint16_t y)
{
return elements[x + y * width + iter * width * height];
}
void Sand::update()
{
for (uint32_t i = 0; i < width; i++)
{
for (uint32_t j = 0; j < height; j++)
{
auto& current_rule = rules[get(i, j)];
unsigned __int128 neighbors = 0;
if (i > 0 && j > 0)
{
neighbors += get(i - 1, j - 1);
}
neighbors <<= 16;
if (j > 0)
{
neighbors += get(i, j - 1);
}
neighbors <<= 16;
if (i < width - 1 && j > 0)
{
neighbors += get(i + 1, j);
}
neighbors <<= 16;
if (i > 0)
{
neighbors += get(i - 1, j);
}
neighbors <<= 16;
if (i < width - 1)
{
neighbors += get(i + 1, j);
}
neighbors <<= 16;
if (i > 0 && j < height - 1)
{
neighbors += get(i - 1, j + 1);
}
neighbors <<= 16;
if (j < height - 1)
{
neighbors += get(i, j + 1);
}
neighbors <<= 16;
if (i < width - 1 && j < height - 1)
{
neighbors += get(i + 1, j + 1);
}
uint16_t result = default_conversions[get(i, j)];
if (!current_rule.empty())
{
std::pair<unsigned __int128, uint16_t> element = { neighbors, 0 };
bool (*fn)(decltype(element), decltype(element))
= [](decltype(element) a, decltype(element) b) -> bool
{ return a.first > b.first; };
const auto& found = binary_search(element, current_rule.data(), 0,
current_rule.size(), fn);
if (found.first == neighbors)
{
result = found.second;
}
}
elements[i + j * width + !iter * width * height] = result;
}
}
iter = !iter;
}