627 lines
14 KiB
C++
627 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/log/log.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 tile;
|
|
|
|
struct grid
|
|
{
|
|
util::array<tile, 2> tiles;
|
|
util::array<bool, 2> item_target;
|
|
|
|
util::array<util::hash_set<geom::point<int, 2>>, 2> belts;
|
|
|
|
static grid create();
|
|
|
|
static geom::point<int, 2> const indices[9];
|
|
|
|
static geom::vector<int, 2> const neighbours[4];
|
|
};
|
|
|
|
geom::point<int, 2> const grid::indices[9] =
|
|
{
|
|
{0, 0},
|
|
{1, 0},
|
|
{2, 0},
|
|
{0, 1},
|
|
{1, 1},
|
|
{2, 1},
|
|
{0, 2},
|
|
{1, 2},
|
|
{2, 2},
|
|
};
|
|
|
|
geom::vector<int, 2> 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<empty, source, factory, zoomer>;
|
|
|
|
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<int, 2> start;
|
|
geom::point<int, 2> target;
|
|
float pos;
|
|
};
|
|
|
|
geom::point<int, 2> item_to_cell(geom::point<int, 2> const & p)
|
|
{
|
|
return {geom::idiv(p[0], 3), geom::idiv(p[1], 3)};
|
|
}
|
|
|
|
geom::point<int, 2> cell_center_to_item(geom::point<int, 2> const & p)
|
|
{
|
|
return {p[0] * 3 + 1, p[1] * 3 + 1};
|
|
}
|
|
|
|
geom::point<int, 2> task_sink_to_item(geom::point<int, 2> 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<int, 2> const & p)
|
|
{
|
|
return p[0] >= 0 && p[0] < 3 && p[1] >= 0 && p[1] < 3;
|
|
}
|
|
|
|
bool item_within_grid(geom::point<int, 2> 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<timestamp> received = {};
|
|
|
|
static constexpr int freq = 3;
|
|
};
|
|
|
|
struct map
|
|
{
|
|
struct grid grid;
|
|
|
|
util::hash_map<geom::point<int, 2>, task> tasks;
|
|
|
|
std::vector<item> 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<bool>(rng) ? -1 : 3;
|
|
if (random::uniform<bool>(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<float, 2> 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<float>(p) + geom::vector{0.5f, 0.5f}, geom::cast<float>(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<float>(q - p);
|
|
|
|
auto c = geom::cast<float>(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<struct source>(&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<struct factory>(&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<float>(item.start), geom::cast<float>(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<int, 2> 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<empty>(map_.grid.tiles(*selected_)))
|
|
{
|
|
util::hash_set<color> 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<empty>(map_.grid.tiles(*selected_)))
|
|
{
|
|
util::hash_set<color> 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<struct source>(&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<item> 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<gmtk::factory>(&map_.grid.tiles(cell)))
|
|
{
|
|
if (factory->input == item.type)
|
|
item.type = factory->output;
|
|
}
|
|
}
|
|
|
|
std::vector<geom::point<int, 2>> 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<float>(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<int, 2> screen_size_{1, 1};
|
|
geom::point<int, 2> mouse_{0, 0};
|
|
|
|
gfx::painter painter_;
|
|
|
|
geom::box<float, 2> view_box_;
|
|
|
|
std::optional<geom::point<int, 2>> selected_;
|
|
std::optional<geom::point<int, 2>> 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);
|
|
}
|
|
|
|
}
|