#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 tile; struct grid { util::array tiles; util::array item_target; util::array>, 2> belts; static grid create(); static geom::point const indices[9]; static geom::vector const neighbours[4]; }; geom::point const grid::indices[9] = { {0, 0}, {1, 0}, {2, 0}, {0, 1}, {1, 1}, {2, 1}, {0, 2}, {1, 2}, {2, 2}, }; geom::vector const grid::neighbours[4] = { {1, 0}, {0, 1}, {-1, 0}, {0, -1}, }; struct empty{}; struct source { color type; }; struct factory { color input; color output; }; struct zoomer { struct grid grid; }; using tile_variant = std::variant; struct tile : tile_variant { using tile_variant::variant; }; grid grid::create() { grid result; result.tiles.resize({3, 3}); result.belts.resize({3, 3}); result.item_target.resize({9, 9}, false); return result; } struct item { color type; geom::point start; geom::point target; float pos; }; geom::point item_to_cell(geom::point const & p) { return {geom::idiv(p[0], 3), geom::idiv(p[1], 3)}; } geom::point cell_center_to_item(geom::point const & p) { return {p[0] * 3 + 1, p[1] * 3 + 1}; } geom::point task_sink_to_item(geom::point p) { p[0] *= 3; p[1] *= 3; if (p[0] == -3) p += geom::vector{2, 1}; else if (p[1] == -3) p += geom::vector{1, 2}; else if (p[0] == 9) p += geom::vector{0, 1}; else if (p[1] == 9) p += geom::vector{1, 0}; return p; } bool within_grid(geom::point const & p) { return p[0] >= 0 && p[0] < 3 && p[1] >= 0 && p[1] < 3; } bool item_within_grid(geom::point const & p) { return p[0] >= 0 && p[0] < 9 && p[1] >= 0 && p[1] < 9; } 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 { color type; // In last 15 seconds std::deque received = {}; static constexpr int freq = 3; }; struct map { struct grid grid; util::hash_map, task> tasks; std::vector items; timestamp time = {}; float spawn_timer = 0.f; }; void generate_next_task(random::generator & rng, map & map) { color type; while (true) { type = random::uniform_from(rng, color_values()); if (map.tasks.size() >= 3) break; bool good = true; for (auto const & t : map.tasks) if (t.second.type == type) good = false; if (good) 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.tasks.contains({x, y})) continue; map.tasks[{x, y}] = {type}; break; } } map starting_map(random::generator & rng) { map result; result.grid = grid::create(); generate_next_task(rng, result); return result; } void draw(map const & map, gfx::painter & painter) { float const grid_width = 0.1f; geom::box source_box{{{0.2f, 0.8f}, {0.2f, 0.8f}}}; 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); } auto & grid = map.grid; for (auto p : grid::indices) for (auto q : grid.belts(p)) painter.line(geom::cast(p) + geom::vector{0.5f, 0.5f}, geom::cast(q) + geom::vector{0.5f, 0.5f}, 0.3f, {191, 191, 191, 255}, true); for (auto p : grid::indices) { for (auto q : grid.belts(p)) { for (int i = 0; i < 3; ++i) { geom::vector d = geom::cast(q - p); auto c = geom::cast(p) + geom::vector{0.5f, 0.5f} + d * (i / 3.f); auto n = geom::ort(d); c += d / 6.f; float s = 1.f / 24.f; d *= s; n *= s; painter.triangle(c - d + n, c - d - n, c + d, {255, 127, 0, 255}); } } } for (auto p : grid::indices) { auto const & cell = grid.tiles(p); if (auto source = std::get_if(&cell)) { painter.rect(source_box + geom::vector{p[0] * 1.f, p[1] * 1.f}, to_color(source->type)); painter.text({p[0] + 0.5f, p[1] + 0.5f}, "180/m", {.scale = {0.01f, -0.01f}, .c = {0, 0, 0, 255}}); } else if (auto factory = std::get_if(&cell)) { auto box = source_box + geom::vector{p[0] * 1.f, p[1] * 1.f}; painter.triangle(box.corner(0, 0), box.corner(1, 1), box.corner(0, 1), to_color(factory->input)); painter.triangle(box.corner(0, 0), box.corner(1, 0), box.corner(1, 1), to_color(factory->output)); } } for (auto const & task : map.tasks) { painter.rect(source_box + geom::vector{task.first[0] * 1.f, task.first[1] * 1.f}, to_color(task.second.type)); painter.text(geom::point{task.first[0] + 0.5f, task.first[1] + 0.5f}, std::format("{}/m", task.second.received.size() * task::freq), {.scale = {0.01f, -0.01f}, .c = {0, 0, 0, 255}}); } for (auto const & item : map.items) { auto pos = geom::lerp(geom::cast(item.start), geom::cast(item.target), item.pos) + geom::vector{0.5f, 0.5f}; pos[0] /= 3.f; pos[1] /= 3.f; painter.circle(pos, 0.075f, {0, 0, 0, 255}); painter.circle(pos, 0.05f, to_color(item.type)); } } void draw_selection(geom::point const & p, gfx::painter & painter) { 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_ && std::holds_alternative(map_.grid.tiles(*selected_))) { util::hash_set types; if (map_.tasks.size() == 1) types.insert(map_.tasks.begin()->second.type); else for (auto t : color_values()) types.insert(t); map_.grid.tiles(*selected_) = source{random::uniform_from(rng_, types)}; } } if (event.down && event.key == app::keycode::B) { if (selected_) { if (belt_start_) { auto d = *selected_ - *belt_start_; if (std::abs(d[0]) + std::abs(d[1]) == 1) { if (within_grid(*selected_)) map_.grid.belts(*selected_).erase(*belt_start_); if (map_.grid.belts(*belt_start_).contains(*selected_)) map_.grid.belts(*belt_start_).erase(*selected_); else map_.grid.belts(*belt_start_).insert(*selected_); } belt_start_ = std::nullopt; } else if (within_grid(*selected_)) belt_start_ = *selected_; } } if (event.down && event.key == app::keycode::F) { if (selected_ && std::holds_alternative(map_.grid.tiles(*selected_))) { util::hash_set types; for (auto const & task : map_.tasks) types.insert(task.second.type); if (types.size() == 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_.grid.tiles(*selected_) = gmtk::factory{input, output}; } } if (event.down && event.key == app::keycode::X) { if (selected_) { map_.grid.tiles(*selected_) = empty{}; } } 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; for (auto p : grid::indices) { if (auto source = std::get_if(&map_.grid.tiles(p))) { auto pos = cell_center_to_item(p); if (!map_.grid.item_target(pos)) { auto & item = map_.items.emplace_back(); item.type = source->type; item.start = pos; item.target = pos; map_.grid.item_target(pos) = true; } } } } for (auto & task : map_.tasks) { auto & received = task.second.received; auto threshold = map_.time; threshold.trunc -= 60 / task::freq; while (!received.empty() && received.front() < threshold) received.pop_front(); } std::vector alive_items; for (auto item : map_.items) { if (item.start != item.target) { item.pos += 3.f * dt; if (item.pos < 1.f) { alive_items.push_back(item); continue; } item.pos -= 1.f; } if (item_within_grid(item.target)) map_.grid.item_target(item.target) = false; auto cell = item_to_cell(item.target); if (map_.tasks.contains(cell)) { if (map_.tasks.at(cell).type == item.type) map_.tasks.at(cell).received.push_back(map_.time); continue; } if (item_within_grid(item.target) && item.target == cell_center_to_item(cell)) { if (auto factory = std::get_if(&map_.grid.tiles(cell))) { if (factory->input == item.type) item.type = factory->output; } } std::vector> targets; if (geom::imod(item.target[0], 3) == 1 && geom::imod(item.target[1], 3) == 1) { for (auto q : map_.grid.belts(cell)) { auto t = item.target + (q - cell); if (!item_within_grid(t) || !map_.grid.item_target(t)) targets.push_back(t); } } else { auto s = item.target; if (geom::imod(s[0], 3) == 0) s[0] -= 1; else if (geom::imod(s[0], 3) == 2) s[0] += 1; else if (geom::imod(s[1], 3) == 0) s[1] -= 1; else if (geom::imod(s[1], 3) == 2) s[1] += 1; if (!item_within_grid(s) || !map_.grid.item_target(s)) if (within_grid(item_to_cell(item.target)) && map_.grid.belts(item_to_cell(item.target)).contains(item_to_cell(s))) targets.push_back(s); auto t = item.target - (s - item.target); if (!item_within_grid(t) || !map_.grid.item_target(t)) if (within_grid(item_to_cell(s)) && map_.grid.belts(item_to_cell(s)).contains(item_to_cell(item.target))) targets.push_back(t); } item.start = item.target; if (!targets.empty()) item.target = random::uniform_from(rng_, targets); if (item_within_grid(item.target)) map_.grid.item_target(item.target) = true; alive_items.push_back(item); } map_.items = std::move(alive_items); 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_ = geom::point{x, y}; else if (map_.tasks.contains({x, y})) selected_ = 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); } }