color-fractory/source/application.cpp

666 lines
14 KiB
C++

#include <psemek/app/default_application_factory.hpp>
#include <psemek/gfx/gl.hpp>
#include <psemek/gfx/painter.hpp>
#include <psemek/util/array.hpp>
#include <psemek/random/generator.hpp>
#include <psemek/random/device.hpp>
#include <psemek/random/uniform.hpp>
#include <psemek/util/hash_table.hpp>
#include <psemek/util/enum.hpp>
#include <psemek/util/clock.hpp>
#include <psemek/geom/box.hpp>
#include <psemek/geom/camera.hpp>
#include <psemek/ecs/container.hpp>
#include <psemek/ecs/declare_uuid.hpp>
#include <psemek/log/log.hpp>
#include <boost/container/flat_set.hpp>
#include <variant>
#include <deque>
#include <format>
namespace gmtk
{
using namespace psemek;
psemek_declare_enum(color, std::uint32_t,
(red)
(green)
(blue)
)
gfx::color_rgba to_color(color c)
{
switch (c)
{
case color::red: return {255, 127, 127, 255};
case color::green: return {127, 255, 127, 255};
case color::blue: return {127, 127, 255, 255};
}
throw util::unknown_enum_value_exception{c};
}
struct location
{
int level;
geom::point<int, 2> coords;
geom::point<float, 2> center() const
{
float s = std::pow(3.f, -level);
return {(coords[0] + 0.5f) * s, (coords[1] + 0.5f) * s};
}
geom::box<float, 2> bbox() const
{
float s = std::pow(3.f, -level);
return {{{(coords[0] + 0) * s, (coords[0] + 1) * s}, {(coords[1] + 0) * s, (coords[1] + 1) * s}}};
}
location down() const
{
return {level + 1, {coords[0] * 3 + 1, coords[1] * 3 + 1}};
}
location up() const
{
return {level - 1, {geom::idiv(coords[0], 3), geom::idiv(coords[1], 3)}};
}
friend bool operator == (location const & x, location const & y) = default;
friend auto operator <=> (location const & x, location const & y) = default;
};
struct location_hash
{
std::size_t operator()(location const & x) const noexcept
{
return util::hash_all(x.level, x.coords[0], x.coords[1]);
}
};
struct vertex
{
psemek_ecs_declare_uuid("vertex")
struct location location;
};
struct source
{
psemek_ecs_declare_uuid("source")
color type;
};
struct transformer
{
psemek_ecs_declare_uuid("transformer")
color input;
color output;
};
struct path_vertex
{
psemek_ecs_declare_uuid("path_vertex")
struct location location;
boost::container::flat_set<ecs::handle> belts = {};
};
struct occupied
{
psemek_ecs_declare_uuid("occupied")
ecs::handle entity;
};
struct item
{
psemek_ecs_declare_uuid("item")
color type;
location start;
ecs::handle target = ecs::handle::null();
float state = 0.f;
};
bool within_grid(location const & l)
{
int max = std::pow(3, l.level + 1);
return l.coords[0] >= 0 && l.coords[0] < max && l.coords[1] >= 0 && l.coords[1] < 3;
}
struct timestamp
{
int trunc = 0;
float frac = 0.f;
timestamp & operator += (float dt)
{
frac += dt;
int t = std::floor(frac);
trunc += t;
frac -= t;
return *this;
}
friend auto operator <=> (timestamp const & x, timestamp const & y) = default;
};
struct task
{
psemek_ecs_declare_uuid("task")
color type;
// In last 15 seconds
std::deque<timestamp> received = {};
static constexpr int freq = 3;
};
struct map
{
ecs::container world;
int current_level = 0;
geom::point<int, 2> current_origin = {0, 0};
timestamp time = {};
float spawn_timer = 0.f;
};
template <typename Component, util::uuid UUID>
struct index_base
{
static constexpr util::uuid uuid()
{
return UUID;
}
index_base(ecs::container & world)
: world_(world)
{
world.apply<Component>([this](ecs::handle entity, Component const & v){
index_[v.location] = entity;
});
world.constructor<Component>([this](ecs::handle entity, Component const & v){
index_[v.location] = entity;
});
world.destructor<Component>([this](Component const & v){
index_.erase(v.location);
});
}
std::optional<ecs::handle> find(location const & l) const
{
if (auto it = index_.find(l); it != index_.end())
return it->second;
return std::nullopt;
}
ecs::handle get(location const & l) const
{
if (auto entity = find(l))
return *entity;
return world_.create(Component{l});
}
private:
ecs::container & world_;
util::hash_map<location, ecs::handle, location_hash> index_;
};
using index = index_base<vertex, util::make_uuid("vertex_index")>;
using path_index = index_base<path_vertex, util::make_uuid("path_vertex_index")>;
void generate_next_task(random::generator & rng, map & map)
{
color type;
util::hash_set<color> existing_tasks;
int task_count = 0;
map.world.apply<task const>([&](task const & task)
{
existing_tasks.insert(task.type);
task_count += 1;
});
while (true)
{
type = random::uniform_from(rng, color_values());
if (task_count >= 3)
break;
if (!existing_tasks.contains(type))
break;
}
while (true)
{
int x = random::uniform(rng, 0, 2);
int y = random::uniform<bool>(rng) ? -1 : 3;
if (random::uniform<bool>(rng))
std::swap(x, y);
if (map.world.index<index>().find({0, {x, y}}))
continue;
map.world.create(
vertex{{0, {x, y}}},
task{type}
);
break;
}
}
map starting_map(random::generator & rng)
{
map result;
result.world.index<index>();
result.world.index<path_index>();
generate_next_task(rng, result);
return result;
}
void draw(map & map, gfx::painter & painter)
{
float const grid_width = 0.1f;
for (int x = 0; x <= 3; ++x)
{
painter.line({x, 0.f}, {x, 3.f}, grid_width, gfx::black, true);
painter.line({0.f, x}, {3.f, x}, grid_width, gfx::black, true);
}
map.world.apply<path_vertex const>([&](path_vertex const & vertex)
{
for (auto b : vertex.belts)
{
auto q = map.world.get(b).get<path_vertex const>().location;
painter.line(vertex.location.center(), q.center(), 0.3f, {191, 191, 191, 255}, true);
}
});
map.world.apply<path_vertex const>([&](path_vertex const & vertex)
{
for (auto b : vertex.belts)
{
auto q = map.world.get(b).get<path_vertex const>().location;
geom::vector d = q.center() - vertex.location.center();
auto c = vertex.location.center() + d / 2.f;
auto n = geom::ort(d);
float s = 1.f / 6.f;
d *= s * 0.5f;
n *= s;
painter.triangle(c - d + n, c - d - n, c + d, {255, 127, 0, 255});
}
});
map.world.apply<vertex const, source const>([&](vertex const & v, source const & s)
{
painter.rect(geom::shrink(v.location.bbox(), 0.2f), to_color(s.type));
painter.text(v.location.center(), "180/m", {.scale = {0.01f, -0.01f}, .c = {0, 0, 0, 255}});
});
map.world.apply<vertex const, transformer const>([&](vertex const & v, transformer const & t)
{
auto box = geom::shrink(v.location.bbox(), 0.2f);
painter.triangle(box.corner(0, 0), box.corner(1, 1), box.corner(0, 1), to_color(t.input));
painter.triangle(box.corner(0, 0), box.corner(1, 0), box.corner(1, 1), to_color(t.output));
});
map.world.apply<vertex const, task const>([&](vertex const & v, task const & t)
{
painter.rect(geom::shrink(v.location.bbox(), 0.2f), to_color(t.type));
painter.text(v.location.center(), std::format("{}/m", t.received.size() * task::freq), {.scale = {0.01f, -0.01f}, .c = {0, 0, 0, 255}});
});
map.world.apply<item const>([&](item const & i)
{
geom::point<float, 2> pos;
if (i.target)
pos = geom::lerp(i.start.center(), map.world.get(i.target).get<path_vertex const>().location.center(), i.state);
else
pos = i.start.center();
painter.circle(pos, 0.075f, {0, 0, 0, 255});
painter.circle(pos, 0.05f, to_color(i.type));
});
}
void draw_selection(location const & l, gfx::painter & painter)
{
auto p = l.coords;
painter.line({p[0], p[1]}, {p[0] + 1.f, p[1]}, 0.1f, {255, 0, 255, 255}, true);
painter.line({p[0] + 1.f, p[1]}, {p[0] + 1.f, p[1] + 1.f}, 0.1f, {255, 0, 255, 255}, true);
painter.line({p[0] + 1.f, p[1] + 1.f}, {p[0], p[1] + 1.f}, 0.1f, {255, 0, 255, 255}, true);
painter.line({p[0], p[1] + 1.f}, {p[0], p[1]}, 0.1f, {255, 0, 255, 255}, true);
}
struct application
: app::application
{
application(options const &, context const &)
: rng_{random::device{}}
, map_(starting_map(rng_))
{}
void on_event(app::resize_event const & event) override
{
screen_size_ = event.size;
}
void on_event(app::mouse_move_event const & event) override
{
mouse_ = event.position;
}
void on_event(app::key_event const & event) override
{
if (event.down && event.key == app::keycode::S)
{
if (selected_ && !map_.world.index<index>().find(*selected_))
{
util::hash_set<color> types;
int task_count = 0;
map_.world.apply<task const>([&](task const & t)
{
types.insert(t.type);
task_count += 1;
});
if (task_count > 1)
for (auto t : color_values())
types.insert(t);
map_.world.create(
vertex{*selected_},
source{random::uniform_from(rng_, types)}
);
}
}
if (event.down && event.key == app::keycode::B)
{
if (selected_)
{
if (belt_start_)
{
auto d = selected_->coords - belt_start_->coords;
if (std::abs(d[0]) + std::abs(d[1]) == 1)
{
auto & index = map_.world.index<path_index>();
for (int i = 0; i < 3; ++i)
{
auto p = belt_start_->down();
p.coords += d * i;
auto q = p;
q.coords += d;
auto s = index.get(p);
auto t = index.get(q);
auto & sv = map_.world.get(s).get<path_vertex>();
auto & tv = map_.world.get(t).get<path_vertex>();
if (sv.belts.contains(t))
sv.belts.erase(t);
else
{
if (tv.belts.contains(s))
tv.belts.erase(s);
sv.belts.insert(t);
}
}
}
belt_start_ = std::nullopt;
}
else if (within_grid(*selected_))
belt_start_ = *selected_;
}
}
if (event.down && event.key == app::keycode::F)
{
if (selected_ && !map_.world.index<index>().find(*selected_))
{
util::hash_set<color> types;
int task_count = 0;
map_.world.apply<task const>([&](task const & t)
{
types.insert(t.type);
task_count += 1;
});
if (task_count == 1)
{
for (auto c : color_values())
types.insert(c);
}
color input = random::uniform_from(rng_, types);
types.erase(input);
color output = random::uniform_from(rng_, types);
map_.world.create(
vertex{*selected_},
transformer{input, output}
);
}
}
if (event.down && event.key == app::keycode::X)
{
if (selected_)
{
if (auto entity = map_.world.index<index>().find(*selected_))
if (!map_.world.get(*entity).contains<task>())
map_.world.destroy(*entity);
}
}
if (event.down && event.key == app::keycode::SPACE)
{
generate_next_task(rng_, map_);
}
}
bool running() const override
{
return running_;
}
void stop() override
{
running_ = false;
}
void update() override
{
float const dt = clock_.restart().count();
map_.time += dt;
map_.spawn_timer += 3.f * dt;
if (map_.spawn_timer >= 1.f)
{
map_.spawn_timer -= 1.f;
map_.world.apply<vertex const, source const>(
[&](vertex const & v, source const & s)
{
auto p = v.location.down();
auto t = map_.world.index<path_index>().get(p);
if (!map_.world.get(t).contains<occupied>())
{
auto i = map_.world.create(
item{s.type, p}
);
map_.world.attach(t, occupied{i});
}
}
);
}
map_.world.apply<task>([&](task & t)
{
auto threshold = map_.time;
threshold.trunc -= 60 / task::freq;
while (!t.received.empty() && t.received.front() < threshold)
t.received.pop_front();
});
map_.world.apply<item>([&](ecs::handle entity, item & i)
{
if (i.target)
{
i.state += 3.f * dt;
if (i.state < 1.f)
return;
i.state -= 1.f;
i.start = map_.world.get(i.target).get<path_vertex const>().location;
map_.world.detach<occupied>(i.target);
i.target = ecs::handle::null();
}
else
{
map_.world.detach<occupied>(map_.world.index<path_index>().get(i.start));
}
if (auto cell = map_.world.index<index>().find(i.start.up()))
{
if (auto t = map_.world.get(*cell).get_if<task>())
{
if (t->type == i.type)
t->received.push_back(map_.time);
map_.world.destroy(entity);
return;
}
if (auto t = map_.world.get(*cell).get_if<transformer>())
{
if (t->input == i.type)
i.type = t->output;
}
}
std::vector<ecs::handle> targets;
for (auto b : map_.world.get(map_.world.index<path_index>().get(i.start)).get<path_vertex>().belts)
if (!map_.world.get(b).contains<occupied>())
targets.push_back(b);
if (!targets.empty())
i.target = random::uniform_from(rng_, targets);
if (i.target)
map_.world.attach(i.target, occupied{entity});
else
map_.world.attach(map_.world.index<path_index>().get(i.start), occupied{entity});
});
float aspect_ratio = (screen_size_[0] * 1.f) / screen_size_[1];
view_box_[1] = {-1.f, 4.f};
view_box_[0] = view_box_[1];
view_box_[0] = geom::expand(view_box_[0], (view_box_[1].length() * aspect_ratio - view_box_[0].length()) / 2.f);
selected_ = std::nullopt;
{
auto m = screen_to_grid(geom::cast<float>(mouse_));
int x = std::floor(m[0]);
int y = std::floor(m[1]);
if (x >= 0 && x < 3 && y >= 0 && y < 3)
selected_ = {0, geom::point{x, y}};
else if (map_.world.index<index>().find({0, {x, y}}))
selected_ = {0, geom::point{x, y}};
}
}
void present() override
{
gl::ClearColor(1.f, 1.f, 1.f, 1.f);
gl::Clear(gl::COLOR_BUFFER_BIT);
draw(map_, painter_);
if (selected_)
draw_selection(*selected_, painter_);
painter_.render(geom::orthographic_camera{view_box_}.transform());
}
private:
bool running_ = true;
random::generator rng_;
map map_;
util::clock<> clock_;
geom::vector<int, 2> screen_size_{1, 1};
geom::point<int, 2> mouse_{0, 0};
gfx::painter painter_;
geom::box<float, 2> view_box_;
std::optional<location> selected_;
std::optional<location> belt_start_;
geom::point<float, 2> screen_to_grid(geom::point<float, 2> const & p)
{
return view_box_.corner(
p[0] / screen_size_[0],
1.f - p[1] / screen_size_[1]
);
}
};
}
namespace psemek::app
{
std::unique_ptr<application::factory> make_application_factory()
{
application::options options
{
.name = "GMTK 2024",
};
return default_application_factory<gmtk::application>(options);
}
}