#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include 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 coords; geom::point center() const { float s = std::pow(3.f, -level); return {(coords[0] + 0.5f) * s, (coords[1] + 0.5f) * s}; } geom::box 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 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 received = {}; static constexpr int freq = 3; }; struct map { ecs::container world; int current_level = 0; geom::point current_origin = {0, 0}; timestamp time = {}; float spawn_timer = 0.f; }; template struct index_base { static constexpr util::uuid uuid() { return UUID; } index_base(ecs::container & world) : world_(world) { world.apply([this](ecs::handle entity, Component const & v){ index_[v.location] = entity; }); world.constructor([this](ecs::handle entity, Component const & v){ index_[v.location] = entity; }); world.destructor([this](Component const & v){ index_.erase(v.location); }); } std::optional 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 index_; }; using index = index_base; using path_index = index_base; void generate_next_task(random::generator & rng, map & map) { color type; util::hash_set existing_tasks; int task_count = 0; map.world.apply([&](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(rng) ? -1 : 3; if (random::uniform(rng)) std::swap(x, y); if (map.world.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(); result.world.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 & vertex) { for (auto b : vertex.belts) { auto q = map.world.get(b).get().location; painter.line(vertex.location.center(), q.center(), 0.3f, {191, 191, 191, 255}, true); } }); map.world.apply([&](path_vertex const & vertex) { for (auto b : vertex.belts) { auto q = map.world.get(b).get().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 & 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 & 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 & 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 & i) { geom::point pos; if (i.target) pos = geom::lerp(i.start.center(), map.world.get(i.target).get().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().find(*selected_)) { util::hash_set types; int task_count = 0; map_.world.apply([&](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(); 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(); auto & tv = map_.world.get(t).get(); 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().find(*selected_)) { util::hash_set types; int task_count = 0; map_.world.apply([&](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().find(*selected_)) if (!map_.world.get(*entity).contains()) 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 & v, source const & s) { auto p = v.location.down(); auto t = map_.world.index().get(p); if (!map_.world.get(t).contains()) { auto i = map_.world.create( item{s.type, p} ); map_.world.attach(t, occupied{i}); } } ); } map_.world.apply([&](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([&](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().location; map_.world.detach(i.target); i.target = ecs::handle::null(); } else { map_.world.detach(map_.world.index().get(i.start)); } if (auto cell = map_.world.index().find(i.start.up())) { if (auto t = map_.world.get(*cell).get_if()) { 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()) { if (t->input == i.type) i.type = t->output; } } std::vector targets; for (auto b : map_.world.get(map_.world.index().get(i.start)).get().belts) if (!map_.world.get(b).contains()) 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().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(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().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 screen_size_{1, 1}; geom::point mouse_{0, 0}; gfx::painter painter_; geom::box view_box_; std::optional selected_; std::optional belt_start_; geom::point screen_to_grid(geom::point const & p) { return view_box_.corner( p[0] / screen_size_[0], 1.f - p[1] / screen_size_[1] ); } }; } namespace psemek::app { std::unique_ptr make_application_factory() { application::options options { .name = "GMTK 2024", }; return default_application_factory(options); } }