Brought over a bunch of code from the roguelike and now will use it to generate a random map.

master
Zed A. Shaw 1 month ago
parent 8d3d3b4ec3
commit 2daa1c9bd5
  1. 13
      assets/config.json
  2. 47
      assets/devices.json
  3. 23
      assets/enemies.json
  4. 75
      assets/items.json
  5. 20
      assets/tiles.json
  6. BIN
      assets/wall_simple-256.png
  7. BIN
      assets/wall_with_pillars-256.png
  8. BIN
      assets/wall_with_vines-256.png
  9. 16
      combat.cpp
  10. 13
      combat.hpp
  11. 37
      components.cpp
  12. 64
      components.hpp
  13. 24
      constants.hpp
  14. 24
      devices.cpp
  15. 15
      devices.hpp
  16. 158
      dinkyecs.hpp
  17. 12
      events.hpp
  18. 42
      inventory.cpp
  19. 33
      inventory.hpp
  20. 70
      levelmanager.cpp
  21. 39
      levelmanager.hpp
  22. 62
      lights.cpp
  23. 46
      lights.hpp
  24. 10
      main.cpp
  25. 235
      map.cpp
  26. 75
      map.hpp
  27. 69
      matrix.cpp
  28. 328
      matrix.hpp
  29. 75
      meson.build
  30. 86
      pathing.cpp
  31. 30
      pathing.hpp
  32. 5
      point.hpp
  33. 6
      rand.cpp
  34. 28
      rand.hpp
  35. 96
      save.cpp
  36. 45
      save.hpp
  37. 607
      shiterator.hpp
  38. 66
      spatialmap.cpp
  39. 31
      spatialmap.hpp
  40. 215
      systems.cpp
  41. 18
      systems.hpp
  42. 99
      tests/components.cpp
  43. 39
      tests/dbc.cpp
  44. 60
      tests/dijkstra.json
  45. 71
      tests/inventory.cpp
  46. 40
      tests/levelmanager.cpp
  47. 45
      tests/lighting.cpp
  48. 83
      tests/map.cpp
  49. 343
      tests/matrix.cpp
  50. 51
      tests/pathing.cpp
  51. 103
      tests/save.cpp
  52. 137
      tests/spatialmap.cpp
  53. 25
      tests/tilemap.cpp
  54. 35
      tests/worldbuilder.cpp
  55. 75
      tilemap.cpp
  56. 46
      tilemap.hpp
  57. 220
      tser.hpp
  58. 347
      worldbuilder.cpp
  59. 27
      worldbuilder.hpp

@ -1,5 +1,8 @@
{
"textures": [
"assets/wall_simple-256.png",
"assets/wall_with_vines-256.png",
"assets/wall_with_pillars-256.png",
"assets/wall_texture_test-256.png",
"assets/floor_tile_test-256.png",
"assets/ceiling_test-256.png",
@ -13,5 +16,15 @@
"peasant_girl": "assets/undead_peasant-256.png",
"floor": "assets/floor_tile_test-256.png",
"ceiling": "assets/ceiling_test-256.png"
},
"enemy": {
"HEARING_DISTANCE": 8
},
"player": {
},
"worldgen": {
"enemy_probability": 20,
"empty_room_probability": 10,
"device_probability": 30
}
}

@ -0,0 +1,47 @@
{
"STAIRS_DOWN": {
"id": "STAIRS_DOWN",
"name": "Stairs Down",
"placement": "fixed",
"foreground": [24, 205, 189],
"background": [24, 205, 189],
"description": "Stairs that go down further into the dungeon.",
"inventory_count": 0,
"randomized": false,
"components": [
{"type": "Tile", "config": {"chr": "\u2ac5"}},
{"type": "Device",
"config": {"test": true}, "events": ["Events::GUI::STAIRS_DOWN"]
}
]
},
"STAIRS_UP": {
"id": "STAIRS_UP",
"name": "Stairs Up",
"foreground": [24, 205, 189],
"background": [24, 205, 189],
"description": "Stairs that go up, for the weak.",
"inventory_count": 0,
"placement": "fixed",
"components": [
{"type": "Tile", "config": {"chr": "\u2259"}},
{"type": "Device",
"config": {"test": true}, "events": ["Events::GUI::STAIRS_UP"]
}
]
},
"SPIKE_TRAP": {
"id": "SPIKE_TRAP",
"name": "Spike trap",
"foreground": [24, 205, 189],
"background": [24, 205, 189],
"description": "Spikes stab you from the floor.",
"inventory_count": 0,
"components": [
{"type": "Tile", "config": {"chr": "\u1ac7"}},
{"type": "Device",
"config": {"test": true}, "events": ["Events::GUI::TRAP"]
}
]
}
}

@ -0,0 +1,23 @@
{
"PLAYER_TILE": {
"foreground": [255, 200, 125],
"background": [30, 20, 75],
"components": [
{"type": "Tile", "config": {"chr": "\ua66b"}},
{"type": "Combat", "config": {"hp": 200, "damage": 15}},
{"type": "Motion", "config": {"dx": 0, "dy": 0, "random": false}},
{"type": "LightSource", "config": {"strength": 70, "radius": 2}},
{"type": "EnemyConfig", "config": {"hearing_distance": 5}}
]
},
"EVIL_EYE": {
"foreground": [75, 200, 125],
"background": [30, 20, 75],
"components": [
{"type": "Tile", "config": {"chr": "\u08ac"}},
{"type": "Combat", "config": {"hp": 100, "damage": 50}},
{"type": "Motion", "config": {"dx": 0, "dy": 0, "random": false}},
{"type": "EnemyConfig", "config": {"hearing_distance": 10}}
]
}
}

@ -0,0 +1,75 @@
{
"TORCH_BAD": {
"id": "TORCH_BAD",
"name": "Crappy Torch",
"foreground": [24, 120, 189],
"background": [230,120, 120],
"description": "A torch that barely lights the way. You wonder if it'd be better to not see the person who murders you.",
"inventory_count": 1,
"components": [
{"type": "LightSource", "config": {"strength": 70, "radius": 2.0}},
{"type": "Tile", "config": {"chr": "\u0f08"}}
]
},
"SWORD_RUSTY": {
"id": "SWORD_RUSTY",
"name": "Rusty Junk Sword",
"foreground": [24, 120, 189],
"background": [24, 120, 189],
"description": "A sword left to rot in a deep hole where it acquired a patina of dirt and tetanus. You aren't sure if it's more deadly for you to hold it or for the people you stab with it.",
"inventory_count": 1,
"components": [
{"type": "Weapon", "config": {"damage": 15}},
{"type": "Tile", "config": {"chr": "\u1e37"}}
]
},
"SWORD_LIGHT_AND_FLAME": {
"id": "SWORD_LIGHT_AND_FLAME",
"name": "Sword of Light and Flame",
"foreground": [24, 205, 210],
"background": [24, 205, 210],
"description": "A sword so powerful, a great man from the Land of The Rising Sun thrust it into the ocean of Nerf to chill its effects.",
"inventory_count": 1,
"components": [
{"type": "LightSource", "config": {"strength": 70, "radius": 1.8}},
{"type": "Tile", "config": {"chr": "\u0236"}},
{"type": "Weapon", "config": {"damage": 30}}
]
},
"CHEST_SMALL": {
"id": "CHEST_SMALL",
"name": "Small Chest",
"foreground": [150, 100, 189],
"background": [150, 100, 189],
"description": "A small chest of gold. You wonder who would leave something like this around.",
"components": [
{"type": "Tile", "config": {"chr": "\uaaea"}},
{"type": "Loot", "config": {"amount": 10}}
],
"inventory_count": 1
},
"WALL_TORCH": {
"id": "WALL_TORCH",
"name": "Basic Wall Torch",
"foreground": [24, 205, 210],
"background": [24, 205, 210],
"description": "A torch on a wall you can't pick up.",
"inventory_count": 0,
"components": [
{"type": "Tile", "config": {"chr": "\u077e"}},
{"type": "LightSource", "config": {"strength": 60, "radius": 1.8}}
]
},
"POTION_HEALING_SMALL": {
"id": "POTION_HEALING_SMALL",
"name": "Small Healing Potion",
"foreground": [255, 205, 189],
"background": [255, 205, 189],
"description": "A small healing potion.",
"inventory_count": 1,
"components": [
{"type": "Tile", "config": {"chr": "\u03eb"}},
{"type": "Curative", "config": {"hp": 20}}
]
}
}

@ -0,0 +1,20 @@
{
"WALL_TILE": {
"foreground": [230, 20, 30],
"background": [230, 20, 120],
"collision": true,
"display": "\ua5b8"
},
"FLOOR_TILE": {
"foreground": [40, 15, 125],
"background": [200, 15, 75],
"collision": false,
"display":"\u289e"
},
"CEILING_TILE": {
"foreground": [159, 164, 15],
"background": [199, 15, 79],
"collision": true,
"display":"\u2274"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

@ -0,0 +1,16 @@
#include "combat.hpp"
#include "rand.hpp"
namespace components {
int Combat::attack(Combat &target) {
int attack = Random::uniform<int>(0,1);
int my_dmg = 0;
if(attack) {
my_dmg = Random::uniform<int>(1, damage);
target.hp -= my_dmg;
}
return my_dmg;
}
}

@ -0,0 +1,13 @@
#pragma once
namespace components {
struct Combat {
int hp;
int damage;
/* NOTE: This is used to _mark_ entities as dead, to detect ones that have just died. Don't make attack automatically set it.*/
bool dead = false;
int attack(Combat &target);
};
}

@ -0,0 +1,37 @@
#include "components.hpp"
namespace components {
void configure(DinkyECS::World &world, DinkyECS::Entity entity, json& entity_data) {
for(auto &comp : entity_data["components"]) {
json& config = comp["config"];
const string comp_type = comp["type"];
if(comp_type == "Weapon") {
world.set<Weapon>(entity, {config["damage"]});
} else if(comp_type == "LightSource") {
world.set<LightSource>(entity, {config["strength"], config["radius"]});
} else if(comp_type == "Loot") {
world.set<Loot>(entity, {config["amount"]});
} else if(comp_type == "Tile") {
world.set<Tile>(entity, {config["chr"]});
} else if(comp_type == "EnemyConfig") {
world.set<EnemyConfig>(entity, {config["hearing_distance"]});
} else if(comp_type == "Combat") {
world.set<Combat>(entity, {config["hp"], config["damage"]});
} else if(comp_type == "Curative") {
world.set<Curative>(entity, {config["hp"]});
} else if(comp_type == "Motion") {
world.set<Motion>(entity, {config["dx"], config["dy"], config["random"]});
} else if(comp_type == "Device") {
Device device{.config=config, .events={}};
device.configure_events(comp["events"]);
world.set<Device>(entity, device);
} else {
dbc::sentinel(fmt::format("ITEM COMPONENT TYPE MISSING: {}",
std::string(comp_type)));
}
// json config variable dies
}
}
}

@ -0,0 +1,64 @@
#pragma once
#include "dinkyecs.hpp"
#include "devices.hpp"
#include "combat.hpp"
#include "inventory.hpp"
#include "tser.hpp"
#include "config.hpp"
namespace components {
struct Player {
DinkyECS::Entity entity;
DEFINE_SERIALIZABLE(Player, entity);
};
struct Position {
Point location;
DEFINE_SERIALIZABLE(Position, location);
};
struct Motion {
int dx;
int dy;
bool random=false;
DEFINE_SERIALIZABLE(Motion, dx, dy);
};
struct Loot {
int amount;
DEFINE_SERIALIZABLE(Loot, amount);
};
struct Tile {
std::string chr;
DEFINE_SERIALIZABLE(Tile, chr);
};
struct GameConfig {
Config game;
Config enemies;
Config items;
Config tiles;
Config devices;
};
struct EnemyConfig {
int hearing_distance = 10;
};
struct Debug {
bool PATHS=false;
bool LIGHT=false;
};
struct Weapon {
int damage = 0;
};
struct Curative {
int hp = 10;
};
void configure(DinkyECS::World &world, DinkyECS::Entity entity, json& entity_data);
}

@ -17,3 +17,27 @@ constexpr const bool DEBUG_BUILD=false;
#else
constexpr const bool DEBUG_BUILD=true;
#endif
////////// copied from roguish
constexpr int INV_WALL = 0;
constexpr int INV_SPACE = 1;
constexpr int WALL_VALUE = 1;
constexpr int SPACE_VALUE = 0;
constexpr int WALL_PATH_LIMIT = 1000;
constexpr int WALL_LIGHT_LEVEL = 3;
constexpr int WORLDBUILD_DIVISION = 4;
constexpr int WORLDBUILD_SHRINK = 2;
constexpr int WORLDBUILD_MAX_PATH = 200;
constexpr int VIDEO_WINDOW_X=1600;
constexpr int VIDEO_WINDOW_Y=900;
constexpr int UI_FONT_SIZE=30;
constexpr int BASE_MAP_FONT_SIZE=90;
constexpr int GAME_MAP_PIXEL_POS = 600;
constexpr int MAX_FONT_SIZE = 140;
constexpr int MIN_FONT_SIZE = 20;
constexpr int STATUS_UI_WIDTH = 40;
constexpr int STATUS_UI_HEIGHT = 30;
constexpr float PERCENT = 0.01f;

@ -0,0 +1,24 @@
#include "devices.hpp"
#include "events.hpp"
#include "dbc.hpp"
namespace components {
/*
* Note: This should go away or at least the event names to
* numbers should probably be automatically created.
*/
void Device::configure_events(json &event_names) {
for(string name : event_names) {
if(name == "Events::GUI::STAIRS_DOWN") {
events.push_back(Events::GUI::STAIRS_DOWN);
} else if(name == "Events::GUI::STAIRS_UP") {
events.push_back(Events::GUI::STAIRS_UP);
} else if(name == "Events::GUI::TRAP") {
events.push_back(Events::GUI::TRAP);
} else {
dbc::sentinel(fmt::format("Unknown device event {}", name));
}
}
}
}

@ -0,0 +1,15 @@
#pragma once
#include "dinkyecs.hpp"
#include <nlohmann/json.hpp>
#include <vector>
namespace components {
using namespace nlohmann;
struct Device {
json config;
std::vector<int> events;
void configure_events(json &event_names);
};
}

@ -0,0 +1,158 @@
#pragma once
#include <functional>
#include <typeindex>
#include <typeinfo>
#include <unordered_map>
#include <any>
#include <tuple>
#include <queue>
#include "tser.hpp"
#include "dbc.hpp"
namespace DinkyECS {
typedef unsigned long Entity;
using EntityMap = std::unordered_map<Entity, std::any>;
struct Event {
int event = 0;
Entity entity = 0;
std::any data;
};
typedef std::queue<Event> EventQueue;
struct World {
unsigned long entity_count = 0;
std::unordered_map<std::type_index, EntityMap> $components;
std::unordered_map<std::type_index, std::any> $facts;
std::unordered_map<std::type_index, EventQueue> $events;
std::vector<Entity> $constants;
Entity entity() {
return ++entity_count;
}
void clone_into(DinkyECS::World &to_world) {
to_world.$constants = $constants;
to_world.$facts = $facts;
to_world.entity_count = entity_count;
for(auto eid : $constants) {
for(const auto &[tid, eid_map] : $components) {
auto& their_map = to_world.$components[tid];
if(eid_map.contains(eid)) {
their_map.insert_or_assign(eid, eid_map.at(eid));
}
}
}
}
void make_constant(DinkyECS::Entity entity) {
$constants.push_back(entity);
}
template <typename Comp>
EntityMap& entity_map_for() {
return $components[std::type_index(typeid(Comp))];
}
template <typename Comp>
EventQueue& queue_map_for() {
return $events[std::type_index(typeid(Comp))];
}
template <typename Comp>
void remove(Entity ent) {
EntityMap &map = entity_map_for<Comp>();
map.erase(ent);
}
template <typename Comp>
void set_the(Comp val) {
$facts.insert_or_assign(std::type_index(typeid(Comp)), val);
}
template <typename Comp>
Comp &get_the() {
auto comp_id = std::type_index(typeid(Comp));
dbc::check($facts.contains(comp_id),
fmt::format("!!!! ATTEMPT to access world fact that hasn't been set yet: {}", typeid(Comp).name()));
// use .at to get std::out_of_range if fact not set
std::any &res = $facts.at(comp_id);
return std::any_cast<Comp&>(res);
}
template <typename Comp>
bool has_the() {
auto comp_id = std::type_index(typeid(Comp));
return $facts.contains(comp_id);
}
template <typename Comp>
void set(Entity ent, Comp val) {
EntityMap &map = entity_map_for<Comp>();
map.insert_or_assign(ent, val);
}
template <typename Comp>
Comp &get(Entity ent) {
EntityMap &map = entity_map_for<Comp>();
// use .at for bounds checking
std::any &res = map.at(ent);
return std::any_cast<Comp&>(res);
}
template <typename Comp>
bool has(Entity ent) {
EntityMap &map = entity_map_for<Comp>();
return map.contains(ent);
}
template<typename Comp>
void query(std::function<void(const Entity&, Comp&)> cb) {
EntityMap &map = entity_map_for<Comp>();
for(auto& [entity, any_comp] : map) {
Comp &res = std::any_cast<Comp&>(any_comp);
cb(entity, res);
}
}
template<typename CompA, typename CompB>
void query(std::function<void(const Entity&, CompA&, CompB&)> cb) {
EntityMap &map_a = entity_map_for<CompA>();
EntityMap &map_b = entity_map_for<CompB>();
for(auto& [entity, any_a] : map_a) {
if(map_b.contains(entity)) {
CompA &res_a = std::any_cast<CompA&>(any_a);
CompB &res_b = get<CompB>(entity);
cb(entity, res_a, res_b);
}
}
}
template<typename Comp>
void send(Comp event, Entity entity, std::any data) {
EventQueue &queue = queue_map_for<Comp>();
queue.push({event, entity, data});
}
template<typename Comp>
Event recv() {
EventQueue &queue = queue_map_for<Comp>();
Event evt = queue.front();
queue.pop();
return evt;
}
template<typename Comp>
bool has_event() {
EventQueue &queue = queue_map_for<Comp>();
return !queue.empty();
}
};
}

@ -0,0 +1,12 @@
#pragma once
namespace Events {
enum GUI {
START, COMBAT, LOOT, DEATH, STAIRS_UP, STAIRS_DOWN, TRAP
};
struct Combat {
int player_did;
int enemy_did;
};
}

@ -0,0 +1,42 @@
#include "inventory.hpp"
namespace components {
void Inventory::add(InventoryItem new_item) {
for(auto &slot : items) {
if(new_item.data["id"] == slot.data["id"]) {
slot.count += new_item.count;
return;
}
}
items.push_back(new_item);
}
InventoryItem& Inventory::get(size_t at) {
dbc::check(at < items.size(), fmt::format("inventory index {} too big", at));
return items[at];
}
bool Inventory::decrease(size_t at, int count) {
dbc::check(at < items.size(), fmt::format("inventory index {} too big", at));
auto &slot = items[at];
slot.count -= count;
return slot.count > 0;
}
void Inventory::erase_item(size_t at) {
dbc::check(at < items.size(), fmt::format("inventory index {} too big", at));
items.erase(items.begin() + at);
}
int Inventory::item_index(std::string id) {
for(size_t i = 0; i < items.size(); i++) {
if(items[i].data["id"] == id) {
return i;
}
}
return -1;
}
}

@ -0,0 +1,33 @@
#pragma once
#include "lights.hpp"
#include <nlohmann/json.hpp>
#include <fmt/core.h>
namespace components {
using namespace nlohmann;
using lighting::LightSource;
struct InventoryItem {
int count;
json data;
};
struct Inventory {
int gold=0;
LightSource light{0, 0};
std::vector<InventoryItem> items{};
size_t count() { return items.size(); }
void add(InventoryItem item);
bool decrease(size_t at, int count);
InventoryItem& get(size_t at);
int item_index(std::string id);
void erase_item(size_t at);
};
}

@ -0,0 +1,70 @@
#include "levelmanager.hpp"
#include "worldbuilder.hpp"
#include "constants.hpp"
#include "save.hpp"
#include "systems.hpp"
#include "components.hpp"
using lighting::LightRender;
using std::shared_ptr, std::make_shared;
using namespace components;
LevelManager::LevelManager() {
create_level();
}
LevelScaling LevelManager::scale_level() {
return {
30 + (5 * int($current_level)),
20 + (5 * int($current_level))
};
}
size_t LevelManager::create_level(shared_ptr<DinkyECS::World> prev_world) {
auto world = make_shared<DinkyECS::World>();
if(prev_world != nullptr) {
prev_world->clone_into(*world);
} else {
save::load_configs(*world);
}
auto scaling = scale_level();
auto map = make_shared<Map>(scaling.map_width, scaling.map_height);
WorldBuilder builder(*map);
builder.generate(*world);
size_t index = $levels.size();
auto collider = make_shared<SpatialMap>();
// not sure if this is still needed
System::init_positions(*world, *collider);
$levels.emplace_back(index, map, world,
make_shared<LightRender>(map->width(), map->height()),
collider);
dbc::check(index == $levels.size() - 1, "Level index is not the same as $levels.size() - 1, off by one error");
return index;
}
GameLevel &LevelManager::next() {
dbc::check($current_level < $levels.size(), "attempt to get next level when at end");
$current_level++;
return $levels.at($current_level);
}
GameLevel &LevelManager::previous() {
dbc::check($current_level > 0, "attempt to go to previous level when at 0");
$current_level--;
return $levels.at($current_level);
}
GameLevel &LevelManager::current() {
return $levels.at($current_level);
}
GameLevel &LevelManager::get(size_t index) {
return $levels.at(index);
}

@ -0,0 +1,39 @@
#pragma once
#include "dinkyecs.hpp"
#include "lights.hpp"
#include "map.hpp"
#include <vector>
#include <memory>
#include "spatialmap.hpp"
using std::shared_ptr;
struct GameLevel {
size_t index;
shared_ptr<Map> map;
shared_ptr<DinkyECS::World> world;
shared_ptr<lighting::LightRender> lights;
shared_ptr<SpatialMap> collision;
};
struct LevelScaling {
int map_width=40;
int map_height=50;
};
class LevelManager {
public:
std::vector<GameLevel> $levels;
size_t $current_level = 0;
LevelManager();
size_t create_level(shared_ptr<DinkyECS::World> prev_world = nullptr);
GameLevel &next();
GameLevel &previous();
GameLevel &current();
size_t current_index() { return $current_level; }
GameLevel &get(size_t index);
LevelScaling scale_level();
};

@ -0,0 +1,62 @@
#include "lights.hpp"
#include "constants.hpp"
#include <vector>
using std::vector;
namespace lighting {
void LightRender::render_square_light(LightSource source, Point at, PointList &has_light) {
for(matrix::box it{$lightmap, at.x, at.y, (size_t)floor(source.radius)}; it.next();) {
if($paths.$paths[it.y][it.x] != WALL_PATH_LIMIT) {
$lightmap[it.y][it.x] = light_level(source.strength, it.distance(), it.x, it.y);
has_light.emplace_back(it.x, it.y);
}
}
}
/*
* NOTE: This really doesn't need to calculate light all the time. It doesn't
* change around the light source until the lightsource is changed, so the
* light levels could be placed in a Matrix inside LightSource, calculated once
* and then simply "applied" to the area where the entity is located. The only
* thing that would need to be calculated each time is the walls.
*/
void LightRender::render_light(LightSource source, Point at) {
Point min, max;
clear_light_target(at);
PointList has_light;
render_square_light(source, at, has_light);
for(auto point : has_light) {
for(matrix::compass it{$lightmap, point.x, point.y}; it.next();) {
if($paths.$paths[it.y][it.x] == WALL_PATH_LIMIT) {
$lightmap[it.y][it.x] = light_level(source.strength, 1.5f, point.x, point.y);
}
}
}
}
int LightRender::light_level(int strength, float distance, size_t x, size_t y) {
int new_level = distance <= 1.0f ? strength : strength / sqrt(distance);
int cur_level = $lightmap[y][x];
return cur_level < new_level ? new_level : cur_level;
}
void LightRender::reset_light() {
matrix::assign($lightmap, lighting::MIN);
}
void LightRender::clear_light_target(const Point &at) {
$paths.clear_target(at);
}
void LightRender::set_light_target(const Point &at, int value) {
$paths.set_target(at, value);
}
void LightRender::path_light(Matrix &walls) {
$paths.compute_paths(walls);
}
}

@ -0,0 +1,46 @@
#pragma once
#include <array>
#include "dbc.hpp"
#include "point.hpp"
#include <algorithm>
#include "matrix.hpp"
#include "pathing.hpp"
namespace lighting {
struct LightSource {
int strength = 0;
float radius = 1.0f;
};
const int MIN = 30;
const int MAX = 105;
class LightRender {
public:
size_t $width;
size_t $height;
Matrix $lightmap;
Pathing $paths;
LightRender(size_t width, size_t height) :
$width(width),
$height(height),
$lightmap(height, matrix::Row(width, 0)),
$paths(width, height)
{}
void reset_light();
void set_light_target(const Point &at, int value=0);
void clear_light_target(const Point &at);
void path_light(Matrix &walls);
void light_box(LightSource source, Point from, Point &min_out, Point &max_out);
int light_level(int level, float distance, size_t x, size_t y);
void render_light(LightSource source, Point at);
void render_square_light(LightSource source, Point at, PointList &has_light);
void render_compass_light(LightSource source, Point at, PointList &has_light);
void render_circle_light(LightSource source, Point at, PointList &has_light);
Matrix &lighting() { return $lightmap; }
Matrix &paths() { return $paths.paths(); }
};
}

@ -7,15 +7,15 @@
#include "stats.hpp"
Matrix MAP{
{1,1,1,1,1,1,1,1,1},
{1,1,1,3,2,3,1,1,1},
{1,0,1,0,0,0,0,0,1},
{1,0,1,0,0,1,1,0,1},
{1,0,0,0,0,0,0,0,1},
{1,1,0,0,0,0,0,1,1},
{1,0,0,1,1,1,0,0,1},
{3,0,0,0,0,0,0,0,3},
{2,1,0,0,0,0,0,1,2},
{3,0,0,1,1,1,0,0,3},
{1,0,0,0,0,0,1,1,1},
{1,0,0,0,0,0,0,0,1},
{1,1,1,1,1,1,1,1,1}
{1,1,1,3,2,3,1,1,1}
};
void draw_gui(sf::RenderWindow &window, sf::Text &text, Stats &stats) {

@ -0,0 +1,235 @@
#include "map.hpp"
#include "dbc.hpp"
#include "rand.hpp"
#include <vector>
#include <array>
#include <fmt/core.h>
#include <utility>
#include "matrix.hpp"
using std::vector, std::pair;
using namespace fmt;
Map::Map(size_t width, size_t height) :
$width(width),
$height(height),
$tiles(width, height),
$walls(height, matrix::Row(width, SPACE_VALUE)),
$paths(width, height)
{}
Map::Map(Matrix &walls, Pathing &paths) :
$tiles(matrix::width(walls), matrix::height(walls)),
$walls(walls),
$paths(paths)
{
$width = matrix::width(walls);
$height = matrix::height(walls);
}
void Map::make_paths() {
INVARIANT();
$paths.compute_paths($walls);
}
bool Map::inmap(size_t x, size_t y) {
return x < $width && y < $height;
}
void Map::set_target(const Point &at, int value) {
$paths.set_target(at, value);
}
void Map::clear_target(const Point &at) {
$paths.clear_target(at);
}
bool Map::place_entity(size_t room_index, Point &out) {
dbc::check(room_index < $rooms.size(), "room_index is out of bounds, not enough rooms");
Room &start = $rooms[room_index];
for(matrix::rando_rect it{$walls, start.x, start.y, start.width, start.height}; it.next();) {
if(!iswall(it.x, it.y)) {
out.x = it.x;
out.y = it.y;
return true;
}
}
return false;
}
bool Map::iswall(size_t x, size_t y) {
return $walls[y][x] == WALL_VALUE;
}
void Map::dump(int show_x, int show_y) {
matrix::dump("WALLS", walls(), show_x, show_y);
matrix::dump("PATHS", paths(), show_x, show_y);
}
bool Map::can_move(Point move_to) {
return inmap(move_to.x, move_to.y) &&
!iswall(move_to.x, move_to.y);
}
Point Map::map_to_camera(const Point &loc, const Point &cam_orig) {
return {loc.x - cam_orig.x, loc.y - cam_orig.y};
}
Point Map::center_camera(const Point &around, size_t view_x, size_t view_y) {
int high_x = int(width() - view_x);
int high_y = int(height() - view_y);
int center_x = int(around.x - view_x / 2);
int center_y = int(around.y - view_y / 2);
size_t start_x = high_x > 0 ? std::clamp(center_x, 0, high_x) : 0;
size_t start_y = high_y > 0 ? std::clamp(center_y, 0, high_y) : 0;
return {start_x, start_y};
}
/*
* Finds the next optimal neighbor in the path
* using either a direct or random method.
*
* Both modes will pick a random direction to start
* looking for the next path, then it goes clock-wise
* from there.
*
* In the direct method it will attempt to find
* a path that goes 1 lower in the dijkstra map
* path, and if it can't find that it will go to
* a 0 path (same number).
*
* In random mode it will pick either the next lower
* or the same level depending on what it finds first.
* Since the starting direction is random this will
* give it a semi-random walk that eventually gets to
* the target.
*
* In map generation this makes random paths and carves
* up the space to make rooms more irregular.
*
* When applied to an enemy they will either go straight
* to the player (random=false) or they'll wander around
* drunkenly gradually reaching the player, and dodging
* in and out.
*/
bool Map::neighbors(Point &out, bool random) {
Matrix &paths = $paths.$paths;
bool zero_found = false;
// just make a list of the four directions
std::array<Point, 4> dirs{{
{out.x,out.y-1}, // north
{out.x+1,out.y}, // east
{out.x,out.y+1}, // south
{out.x-1,out.y} // west
}};
// get the current dijkstra number
int cur = paths[out.y][out.x];
// pick a random start of directions
// BUG: is uniform inclusive of the dir.size()?
int rand_start = Random::uniform<int>(0, dirs.size());
// go through all possible directions
for(size_t i = 0; i < dirs.size(); i++) {
// but start at the random start, effectively randomizing
// which valid direction to go
// BUG: this might be wrong given the above ranom from 0-size
Point dir = dirs[(i + rand_start) % dirs.size()];
if(!inmap(dir.x, dir.y)) continue; //skip unpathable stuff
int weight = cur - paths[dir.y][dir.x];
if(weight == 1) {
// no matter what we follow direct paths
out = dir;
return true;
} else if(random && weight == 0) {
// if random is selected and it's a 0 path take it
out = dir;
return true;
} else if(weight == 0) {
// otherwise keep the last zero path for after
out = dir;
zero_found = true;
}
}
// if we reach this then either zero was found and
// zero_found is set true, or it wasn't and nothing found
return zero_found;
}
bool Map::INVARIANT() {
using dbc::check;
check($walls.size() == height(), "walls wrong height");
check($walls[0].size() == width(), "walls wrong width");
check($paths.$width == width(), "in Map paths width don't match map width");
check($paths.$height == height(), "in Map paths height don't match map height");
for(auto room : $rooms) {
check(int(room.x) >= 0 && int(room.y) >= 0,
format("room invalid position {},{}",
room.x, room.y));
check(int(room.width) > 0 && int(room.height) > 0,
format("room has invalid dims {},{}",
room.width, room.height));
}
return true;
}
void Map::load_tiles() {
$tiles.load($walls);
}
void Map::expand() {
// adjust width first
for(auto &row : $walls) {
row.insert(row.begin(), WALL_VALUE);
row.push_back(WALL_VALUE);
}
$width = matrix::width($walls);
// then add two new rows top/bottom of that new width
$walls.insert($walls.begin(), matrix::Row($width, WALL_VALUE));
$walls.push_back(matrix::Row($width, WALL_VALUE));
// now we have the new height
$height = matrix::height($walls);
// reset the pathing and tiles and done
$paths = Pathing($width, $height);
$tiles = TileMap($width, $height);
}
void Map::add_room(Room &room) {
room.x++;
room.y++;
room.width--;
room.height--;
if(room.x + room.width >= $width) {
// fix the width
room.x--;
}
if(room.y + room.height >= $height) {
// fix the height
room.y--;
}
$rooms.push_back(room);
}
void Map::invert_space() {
for(matrix::each_cell it{$walls}; it.next();) {
int is_wall = !$walls[it.y][it.x];
$walls[it.y][it.x] = is_wall;
}
}

@ -0,0 +1,75 @@
#pragma once
#include <vector>
#include <utility>
#include <string>
#include <random>
#include <algorithm>
#include <fmt/core.h>
#include "point.hpp"
#include "tser.hpp"
#include "lights.hpp"
#include "pathing.hpp"
#include "matrix.hpp"
#include "constants.hpp"
#include "tilemap.hpp"
using lighting::LightSource;
struct Room {
size_t x = 0;
size_t y = 0;
size_t width = 0;
size_t height = 0;
Point entry{(size_t)-1, (size_t)-1};
Point exit{(size_t)-1, (size_t)-1};
DEFINE_SERIALIZABLE(Room, x, y, width, height);
};
class Map {
public:
size_t $width;
size_t $height;
TileMap $tiles;
Matrix $walls;
Pathing $paths;
std::vector<Room> $rooms;
Map(size_t width, size_t height);
Map(Matrix &walls, Pathing &paths);
Matrix& paths() { return $paths.paths(); }
TileMap& tiles() { return $tiles; }
Matrix& input_map() { return $paths.input(); }
Matrix& walls() { return $walls; }
size_t width() { return $width; }
size_t height() { return $height; }
int distance(Point to) { return $paths.distance(to); }
Room &room(size_t at) { return $rooms[at]; }
size_t room_count() { return $rooms.size(); }
bool place_entity(size_t room_index, Point &out);
bool inmap(size_t x, size_t y);
bool iswall(size_t x, size_t y);
bool can_move(Point move_to);
// BUG: this isn't really neighbors anymore. Maybe move? Walk?
bool neighbors(Point &out, bool random=false);
void make_paths();
void set_target(const Point &at, int value=0);
void clear_target(const Point &at);
Point map_to_camera(const Point &loc, const Point &cam_orig);
Point center_camera(const Point &around, size_t view_x, size_t view_y);
void expand();
void dump(int show_x=-1, int show_y=-1);
bool INVARIANT();
void load_tiles();
void add_room(Room &room);
void invert_space();
};

@ -3,77 +3,12 @@
#include <fmt/core.h>
#include <cmath>
#include <cstdlib>
#include "constants.hpp"
using namespace fmt;
using std::min, std::max;
namespace matrix {
flood::flood(Matrix &mat, Point start, int old_val, int new_val) :
mat(mat), start(start), old_val(old_val), new_val(new_val),
x(start.x), y(start.y), dirs{mat, start.x, start.y}
{
dbc::check(old_val != new_val, "what you doing?");
current_loc = start;
q.push(start);
}
bool flood::next() {
if(!q.empty()) {
if(!dirs.next()) {
// box is done reset it
auto current_loc = q.front();
q.pop();
dirs = matrix::compass{mat, current_loc.x, current_loc.y};
dirs.next();
}
// get the next thing
if(mat[dirs.y][dirs.x] <= old_val) {
mat[dirs.y][dirs.x] = new_val;
x = dirs.x;
y = dirs.y;
q.push({.x=dirs.x, .y=dirs.y});
}
return true;
} else {
return false;
}
}
line::line(Point start, Point end) :
x(start.x), y(start.y),
x1(end.x), y1(end.y)
{
dx = std::abs(x1 - x);
sx = x < x1 ? 1 : -1;
dy = std::abs(y1 - y) * -1;
sy = y < y1 ? 1 : -1;
error = dx + dy;
}
bool line::next() {
if(x != x1 || y != y1) {
int e2 = 2 * error;
if(e2 >= dy) {
error = error + dy;
x = x + sx;
}
if(e2 <= dx) {
error = error + dx;
y = y + sy;
}
return true;
} else {
return false;
}
}
void dump(const std::string &msg, Matrix &map, int show_x, int show_y) {
println("----------------- {}", msg);
@ -82,6 +17,8 @@ namespace matrix {
if(int(it.x) == show_x && int(it.y) == show_y) {
print("{:x}<", cell);
} else if(cell == WALL_PATH_LIMIT) {
print("# ");
} else if(cell > 15) {
print("* ");
} else {

@ -3,325 +3,49 @@
#include <queue>
#include <string>
#include <array>
#include <numeric>
#include <algorithm>
#include <fmt/core.h>
#include "point.hpp"
#include "rand.hpp"
#include "dbc.hpp"
#include "shiterator.hpp"
namespace matrix {
using std::vector, std::queue, std::array;
using std::min, std::max, std::floor;
using Row = shiterator::BaseRow<int>;
using Matrix = shiterator::Base<int>;
template<typename T>
using BaseRow = vector<T>;
using viewport = shiterator::viewport_t<Matrix>;
template<typename T>
using Base = vector<BaseRow<T>>;
using each_cell = shiterator::each_cell_t<Matrix>;
using Row = vector<int>;
using Matrix = vector<Row>;
/*
* Just a quick thing to reset a matrix to a value.
*/
template<typename MAT, typename VAL>
inline void assign(MAT &out, VAL new_value) {
for(auto &row : out) {
row.assign(row.size(), new_value);
}
}
template<typename MAT>
inline bool inbounds(MAT &mat, size_t x, size_t y) {
// since Point.x and Point.y are size_t any negatives are massive
bool res = (y < mat.size()) && (x < mat[0].size());
return res;
}
template<typename MAT>
inline size_t width(MAT &mat) {
return mat[0].size();
}
template<typename MAT>
inline size_t height(MAT &mat) {
return mat.size();
}
template<typename T>
inline Base<T> make_base(size_t width, size_t height) {
Base<T> result(height, BaseRow<T>(width));
return result;
}
inline Matrix make(size_t width, size_t height) {
Matrix result(height, Row(width));
return result;
}
inline size_t next_x(size_t x, size_t width) {
return (x + 1) * ((x + 1) < width);
}
inline size_t next_y(size_t x, size_t y) {
return y + (x == 0);
}
inline bool at_end(size_t y, size_t height) {
return y < height;
}
inline bool end_row(size_t x, size_t width) {
return x == width - 1;
}
using each_row = shiterator::each_row_t<Matrix>;
using box = shiterator::box_t<Matrix>;
using compass = shiterator::compass_t<Matrix>;
using circle = shiterator::circle_t<Matrix>;
using rectangle = shiterator::rectangle_t<Matrix>;
using rando_rect = shiterator::rando_rect_t<Matrix>;
using line = shiterator::line;
void dump(const std::string &msg, Matrix &map, int show_x=-1, int show_y=-1);
template<typename MAT>
struct each_cell_t {
size_t x = ~0;
size_t y = ~0;
size_t width = 0;
size_t height = 0;
each_cell_t(MAT &mat)
{
height = matrix::height(mat);
width = matrix::width(mat);
}
bool next() {
x = next_x(x, width);
y = next_y(x, y);
return at_end(y, height);
}
};
template<typename MAT>
struct viewport_t {
Point start;
// this is the point in the map
size_t x;
size_t y;
// this is the point inside the box, start at 0
size_t view_x = ~0;
size_t view_y = ~0;
// viewport width/height
size_t width;
size_t height;
viewport_t(MAT &mat, Point start, int max_x, int max_y) :
start(start),
x(start.x-1),
y(start.y-1)
{
width = std::min(size_t(max_x), matrix::width(mat) - start.x);
height = std::min(size_t(max_y), matrix::height(mat) - start.y);
fmt::println("viewport_t max_x, max_y {},{} vs matrix {},{}, x={}, y={}",
max_x, max_y, matrix::width(mat), matrix::height(mat), x, y);
}
bool next() {
y = next_y(x, y);
x = next_x(x, width);
view_x = next_x(view_x, width);
view_y = next_y(view_x, view_y);
return at_end(y, height);
}
};
using viewport = viewport_t<Matrix>;
using each_cell = each_cell_t<Matrix>;
template<typename MAT>
struct each_row_t {
size_t x = ~0;
size_t y = ~0;
size_t width = 0;
size_t height = 0;
bool row = false;
each_row_t(MAT &mat) {
height = matrix::height(mat);
width = matrix::width(mat);
}
bool next() {
x = next_x(x, width);
y = next_y(x, y);
row = end_row(x, width);
return at_end(y, height);
}
};
using each_row = each_row_t<Matrix>;
template<typename MAT>
struct box_t {
size_t from_x;
size_t from_y;
size_t x = 0; // these are set in constructor
size_t y = 0; // again, no fancy ~ trick needed
size_t left = 0;
size_t top = 0;
size_t right = 0;
size_t bottom = 0;
box_t(MAT &mat, size_t at_x, size_t at_y, size_t size) :
box_t(mat, at_x, at_y, size, size) {
}
box_t(MAT &mat, size_t at_x, size_t at_y, size_t width, size_t height) :
from_x(at_x), from_y(at_y)
{
size_t h = matrix::height(mat);
size_t w = matrix::width(mat);
// keeps it from going below zero
// need extra -1 to compensate for the first next()
left = max(from_x, width) - width;
x = left - 1; // must be -1 for next()
// keeps it from going above width
right = min(from_x + width + 1, w);
// same for these two
top = max(from_y, height) - height;
y = top - (left == 0);
bottom = min(from_y + height + 1, h);
}
bool next() {
// calc next but allow to go to 0 for next
x = next_x(x, right);
// x will go to 0, which signals new line
y = next_y(x, y); // this must go here
// if x==0 then this moves it to min_x
x = max(x, left);
// and done
return at_end(y, bottom);
}
float distance() {
int dx = from_x - x;
int dy = from_y - y;
return sqrt((dx * dx) + (dy * dy));
inline Matrix make(size_t width, size_t height) {
return shiterator::make<int>(width, height);
}
};
using box = box_t<Matrix>;
template<typename MAT>
struct compass_t {
size_t x = 0; // these are set in constructor
size_t y = 0; // again, no fancy ~ trick needed
array<int, 4> x_dirs{0, 1, 0, -1};
array<int, 4> y_dirs{-1, 0, 1, 0};
size_t max_dirs=0;
size_t dir = ~0;
compass_t(MAT &mat, size_t x, size_t y) :
x(x), y(y)
{
array<int, 4> x_in{0, 1, 0, -1};
array<int, 4> y_in{-1, 0, 1, 0};
for(size_t i = 0; i < 4; i++) {
int nx = x + x_in[i];
int ny = y + y_in[i];
if(matrix::inbounds(mat, nx, ny)) {
x_dirs[max_dirs] = nx;
y_dirs[max_dirs] = ny;
max_dirs++;
}
}
inline bool inbounds(Matrix &mat, size_t x, size_t y) {
return shiterator::inbounds(mat, x, y);
}
bool next() {
dir++;
if(dir < max_dirs) {
x = x_dirs[dir];
y = y_dirs[dir];
return true;
} else {
return false;
inline size_t width(Matrix &mat) {
return shiterator::width(mat);
}
}
};
using compass = compass_t<Matrix>;
struct flood {
Matrix &mat;
Point start;
int old_val;
int new_val;
queue<Point> q;
Point current_loc;
int x;
int y;
matrix::compass dirs;
flood(Matrix &mat, Point start, int old_val, int new_val);
bool next();
bool next_working();
};
struct line {
int x;
int y;
int x1;
int y1;
int sx;
int sy;
int dx;
int dy;
int error;
line(Point start, Point end);
bool next();
};
template<typename MAT>
struct circle_t {
float center_x;
float center_y;
float radius = 0.0f;
int y = 0;
int dx = 0;
int dy = 0;
int left = 0;
int right = 0;
int top = 0;
int bottom = 0;
int width = 0;
int height = 0;
circle_t(MAT &mat, Point center, float radius) :
center_x(center.x), center_y(center.y), radius(radius)
{
width = matrix::width(mat);
height = matrix::height(mat);
top = max(int(floor(center_y - radius)), 0);
bottom = min(int(floor(center_y + radius)), height - 1);
y = top;
inline size_t height(Matrix &mat) {
return shiterator::height(mat);
}
bool next() {
y++;
if(y <= bottom) {
dy = y - center_y;
dx = floor(sqrt(radius * radius - dy * dy));
left = max(0, int(center_x) - dx);
right = min(width, int(center_x) + dx + 1);
return true;
} else {
return false;
inline void assign(Matrix &out, int new_value) {
shiterator::assign(out, new_value);
}
}
};
using circle = circle_t<Matrix>;
}

@ -5,14 +5,17 @@ project('raycaster', 'cpp',
'cpp_args=-D_GLIBCXX_DEBUG=1 -D_GLIBCXX_DEBUG_PEDANTIC=1',
])
# use this for common options only for our executables
cpp_args=[]
# these are passed as override_defaults
exe_defaults = ['warning_level=2', 'werror=true']
cc = meson.get_compiler('cpp')
tracy = dependency('tracy', static: true)
catch2 = dependency('catch2-with-main')
fmt = dependency('fmt', allow_fallback: true)
freetype2 = dependency('freetype2')
json = dependency('nlohmann_json')
freetype2 = dependency('freetype2')
opengl32 = cc.find_library('opengl32', required: true)
winmm = cc.find_library('winmm', required: true)
gdi32 = cc.find_library('gdi32', required: true)
@ -28,49 +31,57 @@ sfml_network = dependency('sfml_network')
sfml_system = dependency('sfml_system')
sfml_window = dependency('sfml_window')
if get_option('tracy_enable') and get_option('buildtype') != 'debugoptimized'
warning('Profiling builds should set --buildtype=debugoptimized')
endif
dependencies = [
fmt, json, opengl32, freetype2,
flac, ogg, vorbis, vorbisfile, vorbisenc,
winmm, gdi32, sfml_audio, sfml_graphics,
sfml_main, sfml_network, sfml_system,
sfml_window, tracy
sfml_window
]
# use this for common options only for our executables
cpp_args=[]
executable('runtests', [
sources = [
'animator.cpp',
'combat.cpp',
'components.cpp',
'config.cpp',
'dbc.cpp',
'devices.cpp',
'inventory.cpp',
'levelmanager.cpp',
'lights.cpp',
'map.cpp',
'matrix.cpp',
'matrix.cpp',
'pathing.cpp',
'rand.cpp',
'raycaster.cpp',
'save.cpp',
'shiterator.hpp',
'spatialmap.cpp',
'stats.cpp',
'systems.cpp',
'texture.cpp',
'tilemap.cpp',
'worldbuilder.cpp',
]
executable('runtests', sources + [
'tests/base.cpp',
'tests/dbc.cpp',
'tests/inventory.cpp',
'tests/levelmanager.cpp',
'tests/lighting.cpp',
'tests/map.cpp',
'tests/matrix.cpp',
'tests/pathing.cpp',
'tests/spatialmap.cpp',
'tests/tilemap.cpp',
'tests/worldbuilder.cpp',
], override_options: exe_defaults,
dependencies: dependencies + [catch2])
executable('zedcaster', [
'dbc.cpp',
'matrix.cpp',
'config.cpp',
'texture.cpp',
'raycaster.cpp',
'animator.cpp',
'stats.cpp',
'main.cpp'
],
executable('zedcaster',
sources + [ 'main.cpp' ],
cpp_args: cpp_args,
override_options: exe_defaults,
dependencies: dependencies)
executable('amtcaster', [
'dbc.cpp',
'config.cpp',
'amt/texture.cpp',
'amt/raycaster.cpp',
'amt/main.cpp'
],
cpp_args: ['-std=c++23'],
override_options: exe_defaults,
dependencies: dependencies)

@ -0,0 +1,86 @@
#include "constants.hpp"
#include "pathing.hpp"
#include "dbc.hpp"
#include <vector>
using std::vector;
inline void add_neighbors(PointList &neighbors, Matrix &closed, size_t y, size_t x) {
for(matrix::box it{closed, x, y, 1}; it.next();) {
if(closed[it.y][it.x] == 0) {
closed[it.y][it.x] = 1;
neighbors.emplace_back(it.x, it.y);
}
}
}
/*
* Used https://github.com/HenrYxZ/dijkstra-map as a reference.
*/
void Pathing::compute_paths(Matrix &walls) {
INVARIANT();
dbc::check(walls[0].size() == $width,
fmt::format("Pathing::compute_paths called with walls.width={} but paths $width={}", walls[0].size(), $width));
dbc::check(walls.size() == $height,
fmt::format("Pathing::compute_paths called with walls.height={} but paths $height={}", walls[0].size(), $height));
// Initialize the new array with every pixel at limit distance
matrix::assign($paths, WALL_PATH_LIMIT);
Matrix closed = walls;
PointList starting_pixels;
PointList open_pixels;
// First pass: Add starting pixels and put them in closed
for(size_t counter = 0; counter < $height * $width; counter++) {
size_t x = counter % $width;
size_t y = counter / $width;
if($input[y][x] == 0) {
$paths[y][x] = 0;
closed[y][x] = 1;
starting_pixels.emplace_back(x,y);
}
}
// Second pass: Add border to open
for(auto sp : starting_pixels) {
add_neighbors(open_pixels, closed, sp.y, sp.x);
}
// Third pass: Iterate filling in the open list
int counter = 1; // leave this here so it's available below
for(; counter < WALL_PATH_LIMIT && !open_pixels.empty(); ++counter) {
PointList next_open;
for(auto sp : open_pixels) {
$paths[sp.y][sp.x] = counter;
add_neighbors(next_open, closed, sp.y, sp.x);
}
open_pixels = next_open;
}
// Last pass: flood last pixels
for(auto sp : open_pixels) {
$paths[sp.y][sp.x] = counter;
}
}
void Pathing::set_target(const Point &at, int value) {
// FUTURE: I'll eventually allow setting this to negatives for priority
$input[at.y][at.x] = value;
}
void Pathing::clear_target(const Point &at) {
$input[at.y][at.x] = 1;
}
bool Pathing::INVARIANT() {
using dbc::check;
check($paths.size() == $height, "paths wrong height");
check($paths[0].size() == $width, "paths wrong width");
check($input.size() == $height, "input wrong height");
check($input[0].size() == $width, "input wrong width");
return true;
}

@ -0,0 +1,30 @@
#pragma once
#include "point.hpp"
#include "matrix.hpp"
#include <functional>
using matrix::Matrix;
class Pathing {
public:
size_t $width;
size_t $height;
Matrix $paths;
Matrix $input;
Pathing(size_t width, size_t height) :
$width(width),
$height(height),
$paths(height, matrix::Row(width, 1)),
$input(height, matrix::Row(width, 1))
{}
void compute_paths(Matrix &walls);
void set_target(const Point &at, int value=0);
void clear_target(const Point &at);
Matrix &paths() { return $paths; }
Matrix &input() { return $input; }
int distance(Point to) { return $paths[to.y][to.x];}
bool INVARIANT();
};

@ -1,5 +1,6 @@
#pragma once
#include <vector>
#include "tser.hpp"
struct Point {
size_t x = 0;
@ -8,11 +9,13 @@ struct Point {
bool operator==(const Point& other) const {
return other.x == x && other.y == y;
}
DEFINE_SERIALIZABLE(Point, x, y);
};
typedef std::vector<Point> PointList;
struct PointHash {
template<> struct std::hash<Point> {
size_t operator()(const Point& p) const {
return std::hash<int>()(p.x) ^ std::hash<int>()(p.y);
}

@ -0,0 +1,6 @@
#include "rand.hpp"
namespace Random {
std::random_device RNG;
std::mt19937 GENERATOR(RNG());
}

@ -0,0 +1,28 @@
#pragma once
#include <random>
namespace Random {
extern std::mt19937 GENERATOR;
template<typename T>
T uniform(T from, T to) {
std::uniform_int_distribution<T> rand(from, to);
return rand(GENERATOR);
}
template<typename T>
T uniform_real(T from, T to) {
std::uniform_real_distribution<T> rand(from, to);
return rand(GENERATOR);
}
template<typename T>
T normal(T from, T to) {
std::normal_distribution<T> rand(from, to);
return rand(GENERATOR);
}
}

@ -0,0 +1,96 @@
#include "save.hpp"
#include <fstream>
#include "dbc.hpp"
#include <fmt/core.h>
#include "config.hpp"
#include <filesystem>
using namespace components;
using namespace fmt;
template<typename CompT>
inline void extract(DinkyECS::World &world, std::map<DinkyECS::Entity, CompT> &into) {
auto from_world = world.entity_map_for<CompT>();
for(auto [entity, value] : from_world) {
into[entity] = std::any_cast<CompT>(value);
}
}
void save::to_file(fs::path path, DinkyECS::World &world, Map &map) {
SaveData save_data;
tser::BinaryArchive archive;
save_data.facts.player = world.get_the<Player>();
save_data.map = MapData{
map.$width, map.$height,
map.$rooms, map.$walls};
// BUG: lights aren't saved/restored
extract<Position>(world, save_data.position);
extract<Combat>(world, save_data.combat);
extract<Motion>(world, save_data.motion);
extract<Tile>(world, save_data.tile);
// extract<Inventory>(world, save_data.inventory);
archive.save(save_data);
std::string_view archive_view = archive.get_buffer();
std::ofstream out(path, std::ios::binary);
out << archive_view;
out.flush();
}
template<typename CompT>
inline void inject(DinkyECS::World &world, std::map<DinkyECS::Entity, CompT> &outof) {
for(auto [entity, value] : outof) {
world.set<CompT>(entity, value);
}
}
void save::from_file(fs::path path, DinkyECS::World &world_out, Map &map_out) {
tser::BinaryArchive archive(0);
dbc::check(fs::exists(path), format("save file does not exist {}", path.string()));
auto size = fs::file_size(path);
if(std::ifstream in_file{path, std::ios::binary}) {
std::string in_data(size, '\0');
if(in_file.read(&in_data[0], size)) {
std::string_view in_view(in_data);
archive.initialize(in_view);
} else {
dbc::sentinel(format("wrong size or error reading {}", path.string()));
}
} else {
dbc::sentinel(format("failed to load file {}", path.string()));
}
auto save_data = archive.load<SaveData>();
world_out.set_the<Player>(save_data.facts.player);
inject<Position>(world_out, save_data.position);
inject<Combat>(world_out, save_data.combat);
inject<Motion>(world_out, save_data.motion);
inject<Tile>(world_out, save_data.tile);
// inject<Inventory>(world_out, save_data.inventory);
size_t width = save_data.map.width;
size_t height = save_data.map.height;
Pathing paths(width, height);
map_out = Map(save_data.map.walls, paths);
save::load_configs(world_out);
}
void save::load_configs(DinkyECS::World &world) {
Config game("./assets/config.json");
Config enemies("./assets/enemies.json");
Config items("./assets/items.json");
Config tiles("./assets/tiles.json");
Config devices("./assets/devices.json");
world.set_the<GameConfig>({
game, enemies, items, tiles, devices
});
}

@ -0,0 +1,45 @@
#pragma once
#include "components.hpp"
#include "map.hpp"
#include "dinkyecs.hpp"
#include "tser.hpp"
#include <filesystem>
#include <string>
#include <map>
namespace save {
namespace fs = std::filesystem;
struct MapData {
size_t width;
size_t height;
std::vector<Room> rooms;
Matrix walls;
DEFINE_SERIALIZABLE(MapData, width, height, rooms, walls);
};
struct Facts {
components::Player player;
DEFINE_SERIALIZABLE(Facts, player);
};
struct SaveData {
Facts facts;
MapData map;
std::map<DinkyECS::Entity, components::Position> position;
std::map<DinkyECS::Entity, components::Motion> motion;
std::map<DinkyECS::Entity, components::Combat> combat;
std::map<DinkyECS::Entity, components::Tile> tile;
// std::map<DinkyECS::Entity, components::Inventory> inventory;
DEFINE_SERIALIZABLE(SaveData, facts, map, position, motion, combat, tile);
};
void to_file(fs::path path, DinkyECS::World &world, Map &map);
void from_file(fs::path path, DinkyECS::World &world_out, Map &map);
void load_configs(DinkyECS::World &world);
}

@ -0,0 +1,607 @@
#pragma once
#include <vector>
#include <queue>
#include <string>
#include <array>
#include <numeric>
#include <algorithm>
#include <fmt/core.h>
#include "point.hpp"
#include "rand.hpp"
#include "dbc.hpp"
/*
* # What is This Shit?
*
* Announcing the Shape Iterators, or "shiterators" for short. The best shite
* for C++ for-loops since that [one youtube
* video](https://www.youtube.com/watch?v=rX0ItVEVjHc) told everyone to
* recreate SQL databases with structs. You could also say these are Shaw's
* Iterators, but either way they are the _shite_. Or are they shit? You decide.
* Maybe they're "shite"?
*
* A shiterator is a simple generator that converts 2D shapes into a 1D stream
* of x/y coordinates. You give it a matrix, some parameters like start, end,
* etc. and each time you call `next()` you get the next viable x/y coordinate to
* complete the shape. This makes them far superior to _any_ existing for-loop
* technology because shiterators operate _intelligently_ in shapes. Other
* [programming pundits](https://www.youtube.com/watch?v=tD5NrevFtbU) will say
* their 7000 line "easy to maintain" switch statements are better at drawing
* shapes, but they're wrong. My way of making a for-loop do stuff is vastly
* superior because it doesn't use a switch _or_ a virtual function _or_
* inheritance at all. That means they have to be the _fastest_. Feel free to run
* them 1000 times and bask in the glory of 1 nanosecond difference performance.
*
* It's science and shite.
*
* More importantly, shiterators are simple and easy to use. They're so easy to
* use you _don't even use the 3rd part of the for-loop_. What? You read that right,
* not only have I managed to eliminate _both_ massive horrible to maintain switches,
* and also avoided virtual functions, but I've also _eliminated one entire part
* of the for-loop_. This obviously makes them way faster than other inferior
* three-clause-loop-trash. Just look at this comparison:
*
* ```cpp
* for(it = trash.begin(); it != trash.end(); it++) {
* std::cout << it << std::endl;
* }
* ```
*
* ```cpp
* for(each_cell it{mat}; it.next();) {
* std::cout << mat[it.y][it.x] << std::endl;
* }
* ```
*
* Obviously this will outperform _any_ iterator invented in the last 30 years, but the best
* thing about shiterators is their composability and ability to work simultaneously across
* multiple matrices in one loop:
*
* ```cpp
* for(line it{start, end}; it.next();) {
* for(compass neighbor{walls, it.x, it.y}; neighbor.next();) {
* if(walls[neighbor.y][neighbor.x] == 1) {
* wall_update[it.y][it.x] = walls[it.y][it.x] + 10;
* }
* }
* }
* ```
*
* This code sample (maybe, because I didn't run it) draws a line from
* `start` to `end` then looks at each neighbor on a compass (north, south, east, west)
* at each point to see if it's set to 1. If it is then it copies that cell over to
* another matrix with +10. Why would you need this? Your Wizard just shot a fireball
* down a corridor and you need to see if anything in the path is within 1 square of it.
*
* You _also_ don't even need to use a for-loop. Yes, you can harken back to the old
* days when we did everything RAW inside a Duff's Device between a while-loop for
* that PERFORMANCE because who cares about maintenance? You're a game developer! Tests?
* Don't need a test if it runs fine on Sony Playstation only. Maintenance? You're moving
* on to the next project in two weeks anyway right?! Use that while-loop and a shiterator
* to really help that next guy:
*
* ```cpp
* box it{walls, center_x, center_y, 20};
* while(it.next()) {
* walls[it.y][it.x] = 1;
* }
* ```
*
* ## Shiterator "Guarantees"
*
* Just like Rust [guarantees no memory leaks](https://github.com/pop-os/cosmic-comp/issues/1133),
* a shiterator tries to ensure a few things, if it can:
*
* 1. All x/y values will be within the Matrix you give it. The `line` shiterator doesn't though.
* 2. They try to not store anything and only calculate the math necessary to linearlize the shape.
* 3. You can store them and incrementally call next to get the next value.
* 4. You should be able to compose them together on the same Matrix or different matrices of the same dimensions.
* 5. Most of them will only require 1 for-loop, the few that require 2 only do this so you can draw the inside of a shape. `circle` is like this.
* 6. They don't assume any particular classes or require subclassing. As long as the type given enables `mat[y][x]` (row major) access then it'll work.
* 7. The matrix given to a shiterator isn't actually attached to it, so you can use one matrix to setup an iterator, then apply the x/y values to any other matrix of the same dimensions. Great for smart copying and transforming.
* 8. More importantly, shiterators _do not return any values from the matrix_. They only do the math for coordinates and leave it to you to work your matrix.
*
* These shiterators are used all over the game to do map rendering, randomization, drawing, nearly everything that involves a shape.
*
* ## Algorithms I Need
*
* I'm currently looking for a few algorithms, so if you know how to do these let me know:
*
* 1. _Flood fill_ This turns out to be really hard because most algorithms require keeping track of visited cells with a queue, recursion, etc.
* 2. _Random rectangle fill_ I have something that mostly works but it's really only random across each y-axis, then separate y-axes are randomized.
* 3. _Dijkstra Map_ I have a Dijkstra algorithm but it's not in this style yet. Look in `worldbuilder.cpp` for my current implementation.
* 4. _Viewport_ Currently working on this but I need to have a rectangle I can move around as a viewport.
*
*
* ## Usage
*
* Check the `matrix.hpp` for an example if you want to make it more conventient for your own type.
*
* ## Thanks
*
* Special thanks to Amit and hirdrac for their help with the math and for
* giving me the initial idea. hirdrac doesn't want to be held responsible for
* this travesty but he showed me that you can do iteration and _not_ use the
* weird C++ iterators. Amit did a lot to show me how to do these calculations
* without branching. Thanks to you both--and to everyone else--for helping me while I
* stream my development.
*
* ### SERIOUS DISCLAIMER
*
* I am horribly bad at trigonometry and graphics algorithms, so if you've got an idea to improve them
* or find a bug shoot me an email at help@learncodethehardway.com.
*/
namespace shiterator { using std::vector, std::queue, std::array; using
std::min, std::max, std::floor;
template<typename T>
using BaseRow = vector<T>;
template<typename T>
using Base = vector<BaseRow<T>>;
template<typename T>
inline Base<T> make(size_t width, size_t height) {
Base<T> result(height, BaseRow<T>(width));
return result;
}
/*
* Just a quick thing to reset a matrix to a value.
*/
template<typename MAT, typename VAL>
inline void assign(MAT &out, VAL new_value) {
for(auto &row : out) {
row.assign(row.size(), new_value);
}
}
/*
* Tells you if a coordinate is in bounds of the matrix
* and therefore safe to use.
*/
template<typename MAT>
inline bool inbounds(MAT &mat, size_t x, size_t y) {
// since Point.x and Point.y are size_t any negatives are massive
return (y < mat.size()) && (x < mat[0].size());
}
/*
* Gives the width of a matrix. Assumes row major (y/x)
* and vector API .size().
*/
template<typename MAT>
inline size_t width(MAT &mat) {
return mat[0].size();
}
/*
* Same as shiterator::width but just the height.
*/
template<typename MAT>
inline size_t height(MAT &mat) {
return mat.size();
}
/*
* These are internal calculations that help
* with keeping track of the next x coordinate.
*/
inline size_t next_x(size_t x, size_t width) {
return (x + 1) * ((x + 1) < width);
}
/*
* Same as next_x but updates the next y coordinate.
* It uses the fact that when x==0 you have a new
* line so increment y.
*/
inline size_t next_y(size_t x, size_t y) {
return y + (x == 0);
}
/*
* Figures out if you're at the end of the shape,
* which is usually when y > height.
*/
inline bool at_end(size_t y, size_t height) {
return y < height;
}
/*
* Determines if you're at the end of a row.
*/
inline bool end_row(size_t x, size_t width) {
return x == width - 1;
}
/*
* Most basic shiterator. It just goes through
* every cell in the matrix in linear order
* with not tracking of anything else.
*/
template<typename MAT>
struct each_cell_t {
size_t x = ~0;
size_t y = ~0;
size_t width = 0;
size_t height = 0;
each_cell_t(MAT &mat)
{
height = shiterator::height(mat);
width = shiterator::width(mat);
}
bool next() {
x = next_x(x, width);
y = next_y(x, y);
return at_end(y, height);
}
};
/*
* This is just each_cell_t but it sets
* a boolean value `bool row` so you can
* tell when you've reached the end of a
* row. This is mostly used for printing
* out a matrix and similar just drawing the
* whole thing with its boundaries.
*/
template<typename MAT>
struct each_row_t {
size_t x = ~0;
size_t y = ~0;
size_t width = 0;
size_t height = 0;
bool row = false;
each_row_t(MAT &mat) {
height = shiterator::height(mat);
width = shiterator::width(mat);
}
bool next() {
x = next_x(x, width);
y = next_y(x, y);
row = end_row(x, width);
return at_end(y, height);
}
};
/*
* This is a CENTERED box, that will create
* a centered rectangle around a point of a
* certain dimension. This kind of needs a
* rewrite but if you want a rectangle from
* a upper corner then use rectangle_t type.
*
* Passing 1 parameter for the size will make
* a square.
*/
template<typename MAT>
struct box_t {
size_t from_x;
size_t from_y;
size_t x = 0; // these are set in constructor
size_t y = 0; // again, no fancy ~ trick needed
size_t left = 0;
size_t top = 0;
size_t right = 0;
size_t bottom = 0;
box_t(MAT &mat, size_t at_x, size_t at_y, size_t size) :
box_t(mat, at_x, at_y, size, size) {
}
box_t(MAT &mat, size_t at_x, size_t at_y, size_t width, size_t height) :
from_x(at_x), from_y(at_y)
{
size_t h = shiterator::height(mat);
size_t w = shiterator::width(mat);
// keeps it from going below zero
// need extra -1 to compensate for the first next()
left = max(from_x, width) - width;
x = left - 1; // must be -1 for next()
// keeps it from going above width
right = min(from_x + width + 1, w);
// same for these two
top = max(from_y, height) - height;
y = top - (left == 0);
bottom = min(from_y + height + 1, h);
}
bool next() {
// calc next but allow to go to 0 for next
x = next_x(x, right);
// x will go to 0, which signals new line
y = next_y(x, y); // this must go here
// if x==0 then this moves it to min_x
x = max(x, left);
// and done
return at_end(y, bottom);
}
/*
* This was useful for doing quick lighting
* calculations, and I might need to implement
* it in other shiterators. It gives the distance
* to the center from the current x/y.
*/
float distance() {
int dx = from_x - x;
int dy = from_y - y;
return sqrt((dx * dx) + (dy * dy));
}
};
/*
* Stupid simple compass shape North/South/East/West.
* This comes up a _ton_ when doing searching, flood
* algorithms, collision, etc. Probably not the
* fastest way to do it but good enough.
*/
template<typename MAT>
struct compass_t {
size_t x = 0; // these are set in constructor
size_t y = 0; // again, no fancy ~ trick needed
array<int, 4> x_dirs{0, 1, 0, -1};
array<int, 4> y_dirs{-1, 0, 1, 0};
size_t max_dirs=0;
size_t dir = ~0;
compass_t(MAT &mat, size_t x, size_t y) :
x(x), y(y)
{
array<int, 4> x_in{0, 1, 0, -1};
array<int, 4> y_in{-1, 0, 1, 0};
for(size_t i = 0; i < 4; i++) {
int nx = x + x_in[i];
int ny = y + y_in[i];
if(shiterator::inbounds(mat, nx, ny)) {
x_dirs[max_dirs] = nx;
y_dirs[max_dirs] = ny;
max_dirs++;
}
}
}
bool next() {
dir++;
if(dir < max_dirs) {
x = x_dirs[dir];
y = y_dirs[dir];
return true;
} else {
return false;
}
}
};
/*
* Draws a line from start to end using a algorithm from
* https://en.wikipedia.org/wiki/Bresenham%27s_line_algorithm
* No idea if the one I picked is best but it's the one
* that works in the shiterator requirements and produced
* good results.
*
* _WARNING_: This one doesn't check if the start/end are
* within your Matrix, as it's assumed _you_ did that
* already.
*/
struct line {
int x;
int y;
int x1;
int y1;
int sx;
int sy;
int dx;
int dy;
int error;
line(Point start, Point end) :
x(start.x), y(start.y),
x1(end.x), y1(end.y)
{
dx = std::abs(x1 - x);
sx = x < x1 ? 1 : -1;
dy = std::abs(y1 - y) * -1;
sy = y < y1 ? 1 : -1;
error = dx + dy;
}
bool next() {
if(x != x1 || y != y1) {
int e2 = 2 * error;
if(e2 >= dy) {
error = error + dy;
x = x + sx;
}
if(e2 <= dx) {
error = error + dx;
y = y + sy;
}
return true;
} else {
return false;
}
}
};
/*
* Draws a simple circle using a fairly naive algorithm
* but one that actually worked. So, so, so, so many
* circle drawing algorithms described online don't work
* or are flat wrong. Even the very best I could find
* did overdrawing of multiple lines or simply got the
* math wrong. Keep in mind, _I_ am bad at this trig math
* so if I'm finding errors in your circle drawing then
* you got problems.
*
* This one is real simple, and works. If you got better
* then take the challenge but be ready to get it wrong.
*/
template<typename MAT>
struct circle_t {
float center_x;
float center_y;
float radius = 0.0f;
int y = 0;
int dx = 0;
int dy = 0;
int left = 0;
int right = 0;
int top = 0;
int bottom = 0;
int width = 0;
int height = 0;
circle_t(MAT &mat, Point center, float radius) :
center_x(center.x), center_y(center.y), radius(radius)
{
width = shiterator::width(mat);
height = shiterator::height(mat);
top = max(int(floor(center_y - radius)), 0);
bottom = min(int(floor(center_y + radius)), height - 1);
y = top;
}
bool next() {
y++;
if(y <= bottom) {
dy = y - center_y;
dx = floor(sqrt(radius * radius - dy * dy));
left = max(0, int(center_x) - dx);
right = min(width, int(center_x) + dx + 1);
return true;
} else {
return false;
}
}
};
/*
* Basic rectangle shiterator, and like box and rando_rect_t you can
* pass only 1 parameter for size to do a square.
*/
template<typename MAT>
struct rectangle_t {
int x;
int y;
int top;
int left;
int width;
int height;
int right;
int bottom;
rectangle_t(MAT &mat, size_t start_x, size_t start_y, size_t size) :
rectangle_t(mat, start_x, start_y, size, size) {
}
rectangle_t(MAT &mat, size_t start_x, size_t start_y, size_t width, size_t height) :
top(start_y),
left(start_x),
width(width),
height(height)
{
size_t h = shiterator::height(mat);
size_t w = shiterator::width(mat);
y = start_y - 1;
x = left - 1; // must be -1 for next()
right = min(start_x + width, w);
y = start_y;
bottom = min(start_y + height, h);
}
bool next() {
x = next_x(x, right);
y = next_y(x, y);
x = max(x, left);
return at_end(y, bottom);
}
};
/*
* WIP: This one is used to place entities randomly but
* could be used for effects like random destruction of floors.
* It simply "wraps" the rectangle_t but randomizes the x/y values
* using a random starting point. This makes it random across the
* x-axis but only partially random across the y.
*/
template<typename MAT>
struct rando_rect_t {
int x;
int y;
int x_offset;
int y_offset;
rectangle_t<MAT> it;
rando_rect_t(MAT &mat, size_t start_x, size_t start_y, size_t size) :
rando_rect_t(mat, start_x, start_y, size, size) {
}
rando_rect_t(MAT &mat, size_t start_x, size_t start_y, size_t width, size_t height) :
it{mat, start_x, start_y, width, height}
{
x_offset = Random::uniform(0, int(width));
y_offset = Random::uniform(0, int(height));
}
bool next() {
bool done = it.next();
x = it.left + ((it.x + x_offset) % it.width);
y = it.top + ((it.y + y_offset) % it.height);
return done;
}
};
/*
* BROKEN: I'm actually not sure what I'm trying to
* do here yet.
*/
template<typename MAT>
struct viewport_t {
Point start;
// this is the point in the map
size_t x;
size_t y;
// this is the point inside the box, start at 0
size_t view_x = ~0;
size_t view_y = ~0;
// viewport width/height
size_t width;
size_t height;
viewport_t(MAT &mat, Point start, int max_x, int max_y) :
start(start),
x(start.x-1),
y(start.y-1)
{
width = std::min(size_t(max_x), shiterator::width(mat) - start.x);
height = std::min(size_t(max_y), shiterator::height(mat) - start.y);
fmt::println("viewport_t max_x, max_y {},{} vs matrix {},{}, x={}, y={}",
max_x, max_y, shiterator::width(mat), shiterator::height(mat), x, y);
}
bool next() {
y = next_y(x, y);
x = next_x(x, width);
view_x = next_x(view_x, width);
view_y = next_y(view_x, view_y);
return at_end(y, height);
}
};
}

@ -0,0 +1,66 @@
#include "spatialmap.hpp"
#include <fmt/core.h>
using namespace fmt;
using DinkyECS::Entity;
void SpatialMap::insert(Point pos, Entity ent) {
table[pos] = ent;
}
void SpatialMap::remove(Point pos) {
table.erase(pos);
}
void SpatialMap::move(Point from, Point to, Entity ent) {
remove(from);
insert(to, ent);
}
bool SpatialMap::occupied(Point at) const {
return table.contains(at);
}
Entity SpatialMap::get(Point at) const {
return table.at(at);
}
/*
* Avoid doing work by using the dy,dx and confirming that
* at.x or at.y is > 0. If either is 0 then there can't be
* a neighbor since that's out of bounds.
*/
inline void find_neighbor(const PointEntityMap &table, EntityList &result, Point at, int dy, int dx) {
// don't bother checking for cells out of bounds
if((dx < 0 && at.x <= 0) || (dy < 0 && at.y <= 0)) {
return;
}
Point cell = {at.x + dx, at.y + dy};
auto it = table.find(cell);
if (it != table.end()) {
result.insert(result.end(), it->second);
}
}
FoundEntities SpatialMap::neighbors(Point cell, bool diag) const {
EntityList result;
// just unroll the loop since we only check four directions
// this also solves the problem that it was detecting that the cell was automatically included as a "neighbor" but it's not
find_neighbor(table, result, cell, 0, 1); // north
find_neighbor(table, result, cell, 0, -1); // south
find_neighbor(table, result, cell, 1, 0); // east
find_neighbor(table, result, cell, -1, 0); // west
if(diag) {
find_neighbor(table, result, cell, 1, -1); // south east
find_neighbor(table, result, cell, -1, -1); // south west
find_neighbor(table, result, cell, 1, 1); // north east
find_neighbor(table, result, cell, -1, 1); // north west
}
return {!result.empty(), result};
}

@ -0,0 +1,31 @@
#pragma once
#include <vector>
#include <unordered_map>
#include "map.hpp"
#include "dinkyecs.hpp"
#include "point.hpp"
typedef std::vector<DinkyECS::Entity> EntityList;
// Point's has is in point.hpp
typedef std::unordered_map<Point, DinkyECS::Entity> PointEntityMap;
struct FoundEntities {
bool found;
EntityList nearby;
};
class SpatialMap {
public:
SpatialMap() {}
void insert(Point pos, DinkyECS::Entity obj);
void move(Point from, Point to, DinkyECS::Entity ent);
void remove(Point pos);
bool occupied(Point pos) const;
DinkyECS::Entity get(Point at) const;
FoundEntities neighbors(Point position, bool diag=false) const;
private:
PointEntityMap table;
};

@ -0,0 +1,215 @@
#include "systems.hpp"
#include <fmt/core.h>
#include <string>
#include <cmath>
#include "rand.hpp"
#include "spatialmap.hpp"
#include "dbc.hpp"
#include "lights.hpp"
#include "events.hpp"
using std::string;
using namespace fmt;
using namespace components;
using lighting::LightSource;
void System::lighting(GameLevel &level) {
auto &light = *level.lights;
auto &world = *level.world;
auto &map = *level.map;
light.reset_light();
world.query<Position>([&](const auto &ent[[maybe_unused]], auto &position) {
light.set_light_target(position.location);
});
light.path_light(map.walls());
world.query<Position, LightSource>([&](const auto &ent[[maybe_unused]], auto &position, auto &lightsource) {
light.render_light(lightsource, position.location);
});
}
void System::enemy_pathing(GameLevel &level) {
auto &world = *level.world;
auto &map = *level.map;
auto player = world.get_the<Player>();
// TODO: this will be on each enemy not a global thing
const auto &player_position = world.get<Position>(player.entity);
map.set_target(player_position.location);
map.make_paths();
world.query<Position, Motion>([&](const auto &ent, auto &position, auto &motion) {
if(ent != player.entity) {
dbc::check(world.has<EnemyConfig>(ent), "enemy is missing config");
const auto &config = world.get<EnemyConfig>(ent);
Point out = position.location; // copy
if(map.distance(out) < config.hearing_distance) {
map.neighbors(out, motion.random);
motion = { int(out.x - position.location.x), int(out.y - position.location.y)};
}
}
});
map.clear_target(player_position.location);
}
void System::init_positions(DinkyECS::World &world, SpatialMap &collider) {
// BUG: instead of separate things maybe just one
// BUG: Collision component that references what is collide
world.query<Position>([&](const auto &ent, auto &pos) {
if(world.has<Combat>(ent)) {
const auto& combat = world.get<Combat>(ent);
if(!combat.dead) {
collider.insert(pos.location, ent);
}
} else {
collider.insert(pos.location, ent);
}
});
}
inline void move_entity(SpatialMap &collider, Map &game_map, Position &position, Motion &motion, DinkyECS::Entity ent) {
Point move_to = {
position.location.x + motion.dx,
position.location.y + motion.dy
};
motion = {0,0}; // clear it after getting it
// it's a wall, skip
if(!game_map.can_move(move_to)) return;
// there's collision skip
if(collider.occupied(move_to)) return;
// all good, do the move
collider.move(position.location, move_to, ent);
position.location = move_to;
}
void System::motion(GameLevel &level) {
auto &map = *level.map;
auto &world = *level.world;
auto &collider = *level.collision;
world.query<Position, Motion>([&](const auto &ent, auto &position, auto &motion) {
// don't process entities that don't move
if(motion.dx != 0 || motion.dy != 0) {
move_entity(collider, map, position, motion, ent);
}
});
}
void System::death(GameLevel &level) {
auto &world = *level.world;
auto &collider = *level.collision;
auto player = world.get_the<Player>();
// BUG: this is where entities can die on top of
// BUG: eachother and overlap their corpse
// BUG: maybe that can be allowed and looting just shows
// BUG: all dead things there?
world.query<Position, Combat>([&](const auto &ent, auto &position, auto &combat) {
// bring out yer dead
if(combat.hp <= 0 && !combat.dead) {
combat.dead = true;
// take them out of collision map
collider.remove(position.location);
if(ent == player.entity) {
world.send<Events::GUI>(Events::GUI::DEATH, ent, {});
} else {
// remove their motion so they're dead
world.remove<Motion>(ent);
}
}
});
}
void System::collision(GameLevel &level) {
auto &collider = *level.collision;
auto &world = *level.world;
auto player = world.get_the<Player>();
const auto& player_position = world.get<Position>(player.entity);
auto& player_combat = world.get<Combat>(player.entity);
// this is guaranteed to not return the given position
auto [found, nearby] = collider.neighbors(player_position.location);
if(found) {
for(auto entity : nearby) {
if(world.has<Combat>(entity)) {
auto& enemy_combat = world.get<Combat>(entity);
Events::Combat result {
player_combat.attack(enemy_combat),
enemy_combat.attack(player_combat)
};
world.send<Events::GUI>(Events::GUI::COMBAT, entity, result);
} else if(world.has<InventoryItem>(entity)) {
auto item = world.get<InventoryItem>(entity);
auto& item_pos = world.get<Position>(entity);
auto& inventory = world.get<Inventory>(player.entity);
if(world.has<LightSource>(entity)) {
inventory.add(item);
auto &new_light = world.get<LightSource>(entity);
world.set<LightSource>(player.entity, new_light);
inventory.light = new_light;
world.remove<LightSource>(entity);
}
if(world.has<Weapon>(entity)) {
inventory.add(item);
auto &weapon = world.get<Weapon>(entity);
player_combat.damage = weapon.damage;
world.remove<Weapon>(entity);
}
if(world.has<Loot>(entity)) {
auto &loot = world.get<Loot>(entity);
inventory.gold += loot.amount;
world.remove<Loot>(entity);
}
if(world.has<Curative>(entity)) {
auto& cure = world.get<Curative>(entity);
player_combat.hp += cure.hp;
world.remove<Curative>(entity);
}
collider.remove(item_pos.location);
world.remove<Tile>(entity);
world.remove<InventoryItem>(entity);
world.send<Events::GUI>(Events::GUI::LOOT, entity, item);
} else if(world.has<Device>(entity)) {
System::device(world, player.entity, entity);
} else {
println("UNKNOWN COLLISION TYPE {}", entity);
}
}
}
}
void System::pickup(DinkyECS::World &world, DinkyECS::Entity actor, DinkyECS::Entity item) {
dbc::pre("System::pickup actor doesn't have inventory", world.has<Inventory>(actor));
dbc::pre("System::pickup item isn't configured with InventoryItem.", world.has<InventoryItem>(item));
auto& inventory = world.get<Inventory>(actor);
auto& invitem = world.get<InventoryItem>(item);
inventory.add(invitem);
}
void System::device(DinkyECS::World &world, DinkyECS::Entity actor, DinkyECS::Entity item) {
auto& device = world.get<Device>(item);
for(int event : device.events) {
world.send<Events::GUI>((Events::GUI)event, actor, device);
}
println("entity {} INTERACTED WITH DEVICE {}", actor, item);
}

@ -0,0 +1,18 @@
#pragma once
#include "components.hpp"
#include "levelmanager.hpp"
namespace System {
using namespace components;
void lighting(GameLevel &level);
void motion(GameLevel &level);
void collision(GameLevel &level);
void death(GameLevel &level);
void enemy_pathing(GameLevel &level);
void init_positions(DinkyECS::World &world, SpatialMap &collider);
void pickup(DinkyECS::World &world, DinkyECS::Entity actor, DinkyECS::Entity item);
void device(DinkyECS::World &world, DinkyECS::Entity actor, DinkyECS::Entity item);
}

@ -0,0 +1,99 @@
#include <catch2/catch_test_macros.hpp>
#include "components.hpp"
#include "dinkyecs.hpp"
using namespace components;
using namespace DinkyECS;
TEST_CASE("all components can work in the world", "[components]") {
World world;
auto ent1 = world.entity();
world.set<Player>(ent1, {ent1});
world.set<Position>(ent1, {{10,1}});
world.set<Motion>(ent1, {1,0});
world.set<Loot>(ent1, {100});
world.set<Inventory>(ent1, {0});
world.set<Tile>(ent1, {"Z"});
world.set<EnemyConfig>(ent1, {4});
auto player = world.get<Player>(ent1);
REQUIRE(player.entity == ent1);
auto position = world.get<Position>(ent1);
REQUIRE(position.location.x == 10);
REQUIRE(position.location.y == 1);
auto motion = world.get<Motion>(ent1);
REQUIRE(motion.dx == 1);
REQUIRE(motion.dy == 0);
auto loot = world.get<Loot>(ent1);
REQUIRE(loot.amount == 100);
auto inv = world.get<Inventory>(ent1);
REQUIRE(inv.gold == 0);
auto tile = world.get<Tile>(ent1);
REQUIRE(tile.chr == "Z");
}
TEST_CASE("all components can be facts", "[components]") {
World world;
auto ent1 = world.entity();
world.set_the<Player>({ent1});
world.set_the<Position>({{10,1}});
world.set_the<Motion>({1,0});
world.set_the<Loot>({100});
world.set_the<Inventory>({0});
world.set_the<Tile>({"Z"});
world.set_the<EnemyConfig>({4});
auto player = world.get_the<Player>();
REQUIRE(player.entity == ent1);
auto position = world.get_the<Position>();
REQUIRE(position.location.x == 10);
REQUIRE(position.location.y == 1);
auto motion = world.get_the<Motion>();
REQUIRE(motion.dx == 1);
REQUIRE(motion.dy == 0);
auto loot = world.get_the<Loot>();
REQUIRE(loot.amount == 100);
auto inv = world.get_the<Inventory>();
REQUIRE(inv.gold == 0);
auto tile = world.get_the<Tile>();
REQUIRE(tile.chr == "Z");
}
TEST_CASE("confirm combat works", "[components]") {
World world;
auto player = world.entity();
auto enemy = world.entity();
world.set<Combat>(player, {100, 10});
world.set<Combat>(enemy, {20, 10});
auto p_fight = world.get<Combat>(player);
REQUIRE(p_fight.hp == 100);
REQUIRE(p_fight.damage == 10);
REQUIRE(p_fight.dead == false);
auto e_fight = world.get<Combat>(enemy);
REQUIRE(e_fight.hp == 20);
REQUIRE(e_fight.damage == 10);
REQUIRE(e_fight.dead == false);
for(int i = 0; e_fight.hp > 0 && i < 100; i++) {
p_fight.attack(e_fight);
}
}
TEST_CASE("MapConfig loads from JSON", "[components]") {
}

@ -0,0 +1,39 @@
#include <catch2/catch_test_macros.hpp>
#include "dbc.hpp"
using namespace dbc;
TEST_CASE("basic feature tests", "[utils]") {
log("Logging a message.");
try {
sentinel("This shouldn't happen.");
} catch(SentinelError) {
log("Sentinel happened.");
}
pre("confirm positive cases work", 1 == 1);
pre("confirm positive lambda", [&]{ return 1 == 1;});
post("confirm positive post", 1 == 1);
post("confirm postitive post with lamdba", [&]{ return 1 == 1;});
check(1 == 1, "one equals 1");
try {
check(1 == 2, "this should fail");
} catch(CheckError err) {
log("check fail worked");
}
try {
pre("failing pre", 1 == 3);
} catch(PreCondError err) {
log("pre fail worked");
}
try {
post("failing post", 1 == 4);
} catch(PostCondError err) {
log("post faile worked");
}
}

@ -0,0 +1,60 @@
[{
"input": [
[1, 1, 1, 0],
[1, 0, 1, 1],
[1, 0, 1, 1],
[1, 1, 1, 1]
],
"walls": [
[0, 0, 0, 0],
[0, 0, 0, 0],
[0, 0, 1, 0],
[0, 0, 1, 0]
],
"expected": [
[1, 1, 1, 0],
[1, 0, 1, 1],
[1, 0, 1000, 2],
[1, 1, 1000, 3]
]
},{
"input": [
[1, 1, 1, 0],
[1, 0, 1, 1],
[1, 0, 1, 1],
[1, 1, 1, 1]
],
"walls": [
[0, 0, 0, 0],
[0, 0, 0, 0],
[0, 0, 1, 0],
[0, 0, 1, 0]
],
"expected": [
[1, 1, 1, 0],
[1, 0, 1, 1],
[1, 0, 1000, 2],
[1, 1, 1000, 3]
]
},
{
"input": [
[1, 1, 1, 0],
[1, 1, 1, 1],
[1, 0, 1, 1],
[1, 1, 1, 1]
],
"walls": [
[0, 0, 0, 0],
[0, 0, 0, 0],
[0, 0, 1, 0],
[0, 0, 1, 0]
],
"expected": [
[2, 2, 1, 0],
[1, 1, 1, 1],
[1, 0, 1000, 2],
[1, 1, 1000, 3]
]
}
]

@ -0,0 +1,71 @@
#include <catch2/catch_test_macros.hpp>
#include <fmt/core.h>
#include <string>
#include "rand.hpp"
#include <nlohmann/json.hpp>
#include <fstream>
#include "components.hpp"
#include "dinkyecs.hpp"
#include "save.hpp"
#include "systems.hpp"
using namespace nlohmann;
using namespace fmt;
using std::string;
using namespace components;
DinkyECS::Entity add_items(DinkyECS::World &world, GameConfig &config) {
auto sword = world.entity();
json& item_data = config.items["SWORD_RUSTY"];
world.set<InventoryItem>(sword, {item_data["inventory_count"], item_data});
components::configure(world, sword, item_data);
return sword;
}
TEST_CASE("basic inventory test", "[inventory]") {
DinkyECS::World world;
save::load_configs(world);
auto& config = world.get_the<GameConfig>();
auto sword = add_items(world, config);
auto player = world.entity();
world.set<Inventory>(player, {});
auto &inventory = world.get<Inventory>(player);
System::pickup(world, player, sword);
REQUIRE(inventory.count() == 1);
// get the item and confirm there is 1
auto &item1 = inventory.get(0);
REQUIRE(item1.count == 1);
int item_at = inventory.item_index("SWORD_RUSTY");
REQUIRE(item_at == 0);
REQUIRE(inventory.item_index("SADFASFSADF") == -1);
System::pickup(world, player, sword);
REQUIRE(item1.count == 2);
System::pickup(world, player, sword);
REQUIRE(item1.count == 3);
System::pickup(world, player, sword);
REQUIRE(inventory.count() == 1);
REQUIRE(item1.count == 4);
inventory.decrease(0, 1);
REQUIRE(item1.count == 3);
inventory.decrease(0, 2);
REQUIRE(item1.count == 1);
bool active = inventory.decrease(0, 1);
REQUIRE(item1.count == 0);
REQUIRE(active == false);
inventory.erase_item(0);
REQUIRE(inventory.count() == 0);
}

@ -0,0 +1,40 @@
#include <catch2/catch_test_macros.hpp>
#include <fmt/core.h>
#include "map.hpp"
#include "dinkyecs.hpp"
#include "worldbuilder.hpp"
#include "save.hpp"
#include "systems.hpp"
#include "spatialmap.hpp"
#include "levelmanager.hpp"
using namespace fmt;
using std::string;
TEST_CASE("basic level manager test", "[levelmanager]") {
LevelManager lm;
// starts off with one already but I need to change that
size_t level1 = lm.current_index();
size_t level2 = lm.create_level();
auto& test1_level = lm.get(level1);
auto& test2_level = lm.get(level2);
REQUIRE(test1_level.map->width() > 0);
REQUIRE(test1_level.map->height() > 0);
REQUIRE(test1_level.index == 0);
REQUIRE(test2_level.map->width() > 0);
REQUIRE(test2_level.map->height() > 0);
REQUIRE(test2_level.index == 1);
auto& cur_level = lm.current();
REQUIRE(cur_level.index == 0);
auto& next_level = lm.next();
REQUIRE(next_level.index == 1);
auto& prev_level = lm.previous();
REQUIRE(prev_level.index == 0);
}

@ -0,0 +1,45 @@
#include <catch2/catch_test_macros.hpp>
#include <fmt/core.h>
#include <nlohmann/json.hpp>
#include <fstream>
#include "map.hpp"
#include "worldbuilder.hpp"
#include "lights.hpp"
#include "point.hpp"
using namespace lighting;
TEST_CASE("lighting a map works", "[lighting]") {
Map map(20,23);
WorldBuilder builder(map);
builder.generate_map();
Point light1, light2;
REQUIRE(map.place_entity(0, light1));
REQUIRE(map.place_entity(1, light1));
LightSource source1{6, 1.0};
LightSource source2{4,3};
LightRender lr(map.width(), map.height());
lr.reset_light();
lr.set_light_target(light1);
lr.set_light_target(light2);
lr.path_light(map.walls());
lr.render_light(source1, light1);
lr.render_light(source2, light2);
lr.clear_light_target(light1);
lr.clear_light_target(light2);
Matrix &lighting = lr.lighting();
matrix::dump("WALLS=====", map.walls(), light1.x, light1.y);
matrix::dump("PATHS=====", lr.paths(), light1.x, light1.y);
matrix::dump("LIGHTING 1", lighting, light1.x, light1.y);
matrix::dump("LIGHTING 2", lighting, light2.x, light2.y);
}

@ -0,0 +1,83 @@
#include <catch2/catch_test_macros.hpp>
#include <fmt/core.h>
#include <nlohmann/json.hpp>
#include <fstream>
#include "map.hpp"
#include "worldbuilder.hpp"
using namespace fmt;
using namespace nlohmann;
using std::string;
json load_test_data(const string &fname) {
std::ifstream infile(fname);
return json::parse(infile);
}
TEST_CASE("camera control", "[map]") {
Map map(20, 20);
WorldBuilder builder(map);
builder.generate_map();
Point center = map.center_camera({10,10}, 5, 5);
REQUIRE(center.x == 8);
REQUIRE(center.y == 8);
Point translation = map.map_to_camera({10,10}, center);
REQUIRE(translation.x == 2);
REQUIRE(translation.y == 2);
}
TEST_CASE("map placement test", "[map:placement]") {
for(int i = 0; i < 50; i++) {
size_t width = Random::uniform<size_t>(9, 21);
size_t height = Random::uniform<size_t>(13, 25);
Map map(width, height);
WorldBuilder builder(map);
builder.generate_rooms();
map.invert_space();
for(size_t rnum = 0; rnum < map.room_count(); rnum++) {
Room &room = map.room(rnum);
Point pos;
REQUIRE(map.place_entity(rnum, pos));
// matrix::dump("ROOM PLACEMENT TEST", map.walls(), pos.x, pos.y);
REQUIRE(!map.iswall(pos.x, pos.y));
REQUIRE(pos.x >= room.x);
REQUIRE(pos.y >= room.y);
REQUIRE(pos.x <= room.x + room.width);
REQUIRE(pos.y <= room.y + room.height);
}
}
}
TEST_CASE("dijkstra algo test", "[map]") {
json data = load_test_data("./tests/dijkstra.json");
for(auto &test : data) {
Matrix expected = test["expected"];
Matrix input = test["input"];
Matrix walls = test["walls"];
Map map(input.size(), input[0].size());
map.$walls = walls;
map.$paths.$input = input;
REQUIRE(map.INVARIANT());
map.make_paths();
Matrix &paths = map.paths();
if(paths != expected) {
println("ERROR! ------");
matrix::dump("EXPECTED", expected);
matrix::dump("RESULT", paths);
}
REQUIRE(map.INVARIANT());
// FIX ME: REQUIRE(paths == expected);
}
}

@ -0,0 +1,343 @@
#include <catch2/catch_test_macros.hpp>
#include <fmt/core.h>
#include <string>
#include "config.hpp"
#include "matrix.hpp"
#include "rand.hpp"
#include "worldbuilder.hpp"
#include <nlohmann/json.hpp>
#include <fstream>
using namespace nlohmann;
using namespace fmt;
using std::string;
using matrix::Matrix;
TEST_CASE("basic matrix iterator", "[matrix:basic]") {
std::ifstream infile("./tests/dijkstra.json");
json data = json::parse(infile);
auto test = data[0];
Matrix walls = test["walls"];
// tests going through straight cells but also
// using two iterators on one matrix (or two)
matrix::each_cell cells{walls};
cells.next(); // kick it off
size_t row_count = 0;
for(matrix::each_row it{walls};
it.next(); cells.next())
{
REQUIRE(walls[cells.y][cells.x] == walls[it.y][it.x]);
row_count += it.row;
}
REQUIRE(row_count == walls.size());
{
// test getting the correct height in the middle
row_count = 0;
matrix::box box{walls, 2,2, 1};
while(box.next()) {
row_count += box.x == box.left;
walls[box.y][box.x] = 3;
}
matrix::dump("2,2 WALLS", walls, 2, 2);
REQUIRE(row_count == 3);
}
{
matrix::dump("1:1 POINT", walls, 1,1);
// confirm boxes have the right number of rows
// when x goes to 0 on first next call
row_count = 0;
matrix::box box{walls, 1, 1, 1};
while(box.next()) {
row_count += box.x == box.left;
}
REQUIRE(row_count == 3);
}
{
matrix::compass star{walls, 1, 1};
while(star.next()) {
println("START IS {},{}=={}", star.x, star.y, walls[star.y][star.x]);
walls[star.y][star.x] = 11;
}
matrix::dump("STAR POINT", walls, 1,1);
}
}
inline void random_matrix(Matrix &out) {
for(size_t y = 0; y < out.size(); y++) {
for(size_t x = 0; x < out[0].size(); x++) {
out[y][x] = Random::uniform<int>(-10,10);
}
}
}
TEST_CASE("thrash matrix iterators", "[matrix]") {
for(int count = 0; count < Random::uniform<int>(10,30); count++) {
size_t width = Random::uniform<size_t>(1, 100);
size_t height = Random::uniform<size_t>(1, 100);
Matrix test(height, matrix::Row(width));
random_matrix(test);
// first make a randomized matrix
matrix::each_cell cells{test};
cells.next(); // kick off the other iterator
for(matrix::each_row it{test};
it.next(); cells.next())
{
REQUIRE(test[cells.y][cells.x] == test[it.y][it.x]);
}
}
}
TEST_CASE("thrash box distance iterators", "[matrix:distance]") {
size_t width = Random::uniform<size_t>(10, 21);
size_t height = Random::uniform<size_t>(10, 25);
Matrix result(height, matrix::Row(width));
matrix::assign(result, 0);
size_t size = Random::uniform<int>(4, 10);
Point target{width/2, height/2};
matrix::box box{result, target.x, target.y, size};
while(box.next()) {
result[box.y][box.x] = box.distance();
}
matrix::dump(format("MAP {}x{} @ {},{}; BOX {}x{}; size: {}",
matrix::width(result), matrix::height(result),
target.x, target.y, box.right - box.left, box.bottom - box.top, size),
result, target.x, target.y);
}
TEST_CASE("thrash box iterators", "[matrix]") {
for(int count = 0; count < 20; count++) {
size_t width = Random::uniform<size_t>(1, 25);
size_t height = Random::uniform<size_t>(1, 33);
Matrix test(height, matrix::Row(width));
random_matrix(test);
// this will be greater than the random_matrix cells
int test_i = Random::uniform<size_t>(20,30);
// go through every cell
for(matrix::each_cell target{test}; target.next();) {
PointList result;
// make a random size box
size_t size = Random::uniform<int>(1, 33);
matrix::box box{test, target.x, target.y, size};
while(box.next()) {
test[box.y][box.x] = test_i;
result.push_back({box.x, box.y});
}
for(auto point : result) {
REQUIRE(test[point.y][point.x] == test_i);
test[point.y][point.x] = 10; // kind of reset it for another try
}
}
}
}
TEST_CASE("thrash compass iterators", "[matrix:compass]") {
for(int count = 0; count < 20; count++) {
size_t width = Random::uniform<size_t>(1, 25);
size_t height = Random::uniform<size_t>(1, 33);
Matrix test(height, matrix::Row(width));
random_matrix(test);
// this will be greater than the random_matrix cells
int test_i = Random::uniform<size_t>(20,30);
// go through every cell
for(matrix::each_cell target{test}; target.next();) {
PointList result;
// make a random size box
matrix::compass compass{test, target.x, target.y};
while(compass.next()) {
test[compass.y][compass.x] = test_i;
result.push_back({compass.x, compass.y});
}
for(auto point : result) {
REQUIRE(test[point.y][point.x] == test_i);
test[point.y][point.x] = 10; // kind of reset it for another try
}
}
}
}
TEST_CASE("prototype line algorithm", "[matrix:line]") {
size_t width = Random::uniform<size_t>(10, 12);
size_t height = Random::uniform<size_t>(10, 15);
Map map(width,height);
// create a target for the paths
Point start{.x=map.width() / 2, .y=map.height()/2};
for(matrix::box box{map.walls(), start.x, start.y, 3};
box.next();)
{
Matrix result = map.walls();
result[start.y][start.x] = 1;
Point end{.x=box.x, .y=box.y};
for(matrix::line it{start, end}; it.next();)
{
REQUIRE(map.inmap(it.x, it.y));
result[it.y][it.x] = 15;
}
result[start.y][start.x] = 15;
// matrix::dump("RESULT AFTER LINE", result, end.x, end.y);
bool f_found = false;
for(matrix::each_cell it{result}; it.next();) {
if(result[it.y][it.x] == 15) {
f_found = true;
break;
}
}
REQUIRE(f_found);
}
}
TEST_CASE("prototype circle algorithm", "[matrix:circle]") {
for(int count = 0; count < 2; count++) {
size_t width = Random::uniform<size_t>(10, 13);
size_t height = Random::uniform<size_t>(10, 15);
int pos_mod = Random::uniform<int>(-3,3);
Map map(width,height);
// create a target for the paths
Point start{.x=map.width() / 2 + pos_mod, .y=map.height()/2 + pos_mod};
for(float radius = 1.0f; radius < 4.0f; radius += 0.1f) {
// use an empty map
Matrix result = map.walls();
for(matrix::circle it{result, start, radius}; it.next();) {
for(int x = it.left; x < it.right; x++) {
// println("top={}, bottom={}, center.y={}, dy={}, left={}, right={}, x={}, y={}", it.top, it.bottom, it.center.y, it.dy, it.left, it.right, x, it.y);
// println("RESULT {},{}", matrix::width(result), matrix::height(result));
REQUIRE(it.y >= 0);
REQUIRE(x >= 0);
REQUIRE(it.y < int(matrix::height(result)));
REQUIRE(x < int(matrix::width(result)));
result[it.y][x] += 1;
}
}
// matrix::dump(format("RESULT AFTER CIRCLE radius {}", radius), result, start.x, start.y);
}
}
}
TEST_CASE("viewport iterator", "[matrix:viewport]") {
size_t width = Random::uniform<size_t>(20, 22);
size_t height = Random::uniform<size_t>(21, 25);
Map map(width,height);
WorldBuilder builder(map);
builder.generate_map();
size_t view_width = width/2;
size_t view_height = height/2;
Point player;
REQUIRE(map.place_entity(1, player));
Point start = map.center_camera(player, view_width, view_height);
size_t end_x = std::min(view_width, map.width() - start.x);
size_t end_y = std::min(view_height, map.height() - start.y);
matrix::viewport it{map.walls(), start, int(view_width), int(view_height)};
for(size_t y = 0; y < end_y; ++y) {
for(size_t x = 0; x < end_x && it.next(); ++x) {
// still working on this
}
}
}
TEST_CASE("random rectangle", "[matrix:rando_rect]") {
for(int i = 0; i < 10; i++) {
size_t width = Random::uniform<size_t>(9, 21);
size_t height = Random::uniform<size_t>(13, 25);
Map map(width, height);
WorldBuilder builder(map);
builder.generate_rooms();
map.invert_space();
auto wall_copy = map.walls();
for(size_t rnum = 0; rnum < map.room_count(); rnum++) {
Room &room = map.room(rnum);
Point pos;
for(matrix::rando_rect it{map.walls(), room.x, room.y, room.width, room.height}; it.next();)
{
if(map.iswall(it.x, it.y)) {
matrix::dump("BAD RECTANGLE SPOT", map.walls(), it.x, it.y);
}
REQUIRE(!map.iswall(it.x, it.y));
REQUIRE(size_t(it.x) >= room.x);
REQUIRE(size_t(it.y) >= room.y);
REQUIRE(size_t(it.x) <= room.x + room.width);
REQUIRE(size_t(it.y) <= room.y + room.height);
wall_copy[it.y][it.x] = wall_copy[it.y][it.x] + 5;
}
}
// matrix::dump("WALLS FILLED", wall_copy);
}
}
TEST_CASE("standard rectangle", "[matrix:rectangle]") {
for(int i = 0; i < 20; i++) {
size_t width = Random::uniform<size_t>(9, 21);
size_t height = Random::uniform<size_t>(13, 25);
Map map(width, height);
WorldBuilder builder(map);
builder.generate_rooms();
map.invert_space();
auto wall_copy = map.walls();
for(size_t rnum = 0; rnum < map.room_count(); rnum++) {
Room &room = map.room(rnum);
Point pos;
for(matrix::rectangle it{map.walls(), room.x, room.y, room.width, room.height}; it.next();)
{
if(map.iswall(it.x, it.y)) {
matrix::dump("BAD RECTANGLE SPOT", map.walls(), it.x, it.y);
}
REQUIRE(!map.iswall(it.x, it.y));
REQUIRE(size_t(it.x) >= room.x);
REQUIRE(size_t(it.y) >= room.y);
REQUIRE(size_t(it.x) <= room.x + room.width);
REQUIRE(size_t(it.y) <= room.y + room.height);
wall_copy[it.y][it.x] = wall_copy[it.y][it.x] + 5;
}
}
// matrix::dump("WALLS FILLED", wall_copy);
}
}

@ -0,0 +1,51 @@
#include <catch2/catch_test_macros.hpp>
#include <fmt/core.h>
#include <nlohmann/json.hpp>
#include <fstream>
#include "pathing.hpp"
#include "matrix.hpp"
using namespace fmt;
using namespace nlohmann;
using std::string;
json load_test_pathing(const string &fname) {
std::ifstream infile(fname);
return json::parse(infile);
}
TEST_CASE("dijkstra algo test", "[pathing]") {
json data = load_test_pathing("./tests/dijkstra.json");
for(auto &test : data) {
Matrix expected = test["expected"];
Matrix walls = test["walls"];
Pathing pathing(walls[0].size(), walls.size());
pathing.$input = test["input"];
REQUIRE(pathing.INVARIANT());
pathing.compute_paths(walls);
REQUIRE(pathing.INVARIANT());
matrix::dump("PATHING RESULT", pathing.$paths);
matrix::dump("PATHING EXPECTED", expected);
REQUIRE(pathing.$paths == expected);
}
}
TEST_CASE("random flood", "[pathing]") {
json data = load_test_pathing("./tests/dijkstra.json");
auto test = data[0];
Matrix expected = test["expected"];
Matrix walls = test["walls"];
Pathing pathing(walls[0].size(), walls.size());
pathing.$input = test["input"];
REQUIRE(pathing.INVARIANT());
pathing.compute_paths(walls);
}

@ -0,0 +1,103 @@
#include <catch2/catch_test_macros.hpp>
#include <fmt/core.h>
#include <string>
#include "dinkyecs.hpp"
#include "components.hpp"
#include "save.hpp"
#include <optional>
#include <iostream>
#include "map.hpp"
#include "worldbuilder.hpp"
#include "tser.hpp"
using namespace fmt;
using std::string;
using namespace components;
enum class Item : char {
RADAR = 'R',
TRAP = 'T',
ORE = 'O'
};
struct Pixel {
int x = 0;
int y = 0;
DEFINE_SERIALIZABLE(Pixel, x, y);
};
struct Robot {
Pixel point;
std::wstring name;
std::optional<Item> item;
DEFINE_SERIALIZABLE(Robot, point, name, item);
};
TEST_CASE("test using tser for serialization", "[config]") {
auto robot = Robot{ Pixel{3,4}, L"BIG NAME", Item::RADAR};
tser::BinaryArchive archive;
archive.save(robot);
std::string_view archive_view = archive.get_buffer();
tser::BinaryArchive archive2(0);
archive2.initialize(archive_view);
auto loadedRobot = archive2.load<Robot>();
REQUIRE(loadedRobot.point.x == robot.point.x);
REQUIRE(loadedRobot.point.y == robot.point.y);
REQUIRE(loadedRobot.name == robot.name);
REQUIRE(loadedRobot.item == robot.item);
}
TEST_CASE("basic save a world", "[save]") {
DinkyECS::World world;
Map map(20, 20);
WorldBuilder builder(map);
builder.generate_map();
// configure a player as a fact of the world
Player player{world.entity()};
world.set_the<Player>(player);
world.set<Position>(player.entity, {10,10});
world.set<Motion>(player.entity, {0, 0});
world.set<Combat>(player.entity, {100, 10});
world.set<Tile>(player.entity, {"@"});
world.set<Inventory>(player.entity, {102});
save::to_file("./savetest.world", world, map);
DinkyECS::World in_world;
Map in_map(0, 0); // this will be changed on load
save::from_file("./savetest.world", in_world, in_map);
Position &position1 = world.get<Position>(player.entity);
Position &position2 = in_world.get<Position>(player.entity);
REQUIRE(position1.location.x == position2.location.x);
REQUIRE(position1.location.y == position2.location.y);
Combat &combat1 = world.get<Combat>(player.entity);
Combat &combat2 = in_world.get<Combat>(player.entity);
REQUIRE(combat1.hp == combat2.hp);
Motion &motion1 = world.get<Motion>(player.entity);
Motion &motion2 = in_world.get<Motion>(player.entity);
REQUIRE(motion1.dx == motion2.dx);
REQUIRE(motion1.dy == motion2.dy);
Tile &tile1 = world.get<Tile>(player.entity);
Tile &tile2 = in_world.get<Tile>(player.entity);
REQUIRE(tile1.chr == tile2.chr);
REQUIRE(map.width() == in_map.width());
REQUIRE(map.height() == in_map.height());
REQUIRE(map.$walls == in_map.$walls);
Inventory &inv = world.get<Inventory>(player.entity);
REQUIRE(inv.gold == 102);
}

@ -0,0 +1,137 @@
#include <catch2/catch_test_macros.hpp>
#include <fmt/core.h>
#include <string>
#include "spatialmap.hpp"
#include "dinkyecs.hpp"
using DinkyECS::Entity;
using namespace fmt;
EntityList require_found(const SpatialMap& collider, Point at, bool diag, size_t expect_size) {
println("TEST require_found at={},{}", at.x, at.y);
auto [found, nearby] = collider.neighbors(at, diag);
REQUIRE(found == true);
REQUIRE(nearby.size() == expect_size);
return nearby;
}
TEST_CASE("confirm basic collision operations", "[collision]") {
DinkyECS::World world;
Entity player = world.entity();
Entity enemy = world.entity();
SpatialMap collider;
collider.insert({11,11}, player);
collider.insert({21,21}, enemy);
{ // not found
auto [found, nearby] = collider.neighbors({1,1});
REQUIRE(!found);
REQUIRE(nearby.empty());
}
// found
EntityList nearby = require_found(collider, {10,10}, true, 1);
REQUIRE(nearby[0] == player);
{ // removed
collider.remove({11,11});
auto [found, nearby] = collider.neighbors({10,10}, true);
REQUIRE(!found);
REQUIRE(nearby.empty());
}
collider.insert({11,11}, player); // setup for the move test
{ // moving, not found
collider.move({11,11}, {12, 12}, player);
auto [found, nearby] = collider.neighbors({10,10}, true);
REQUIRE(!found);
REQUIRE(nearby.empty());
}
nearby = require_found(collider, {11,11}, true, 1);
REQUIRE(nearby[0] == player);
// confirm occupied works
REQUIRE(collider.occupied({12,12}));
REQUIRE(collider.occupied({21,21}));
REQUIRE(!collider.occupied({1,10}));
REQUIRE(collider.get({12,12}) == player);
}
TEST_CASE("confirm multiple entities moving", "[collision]") {
DinkyECS::World world;
Entity player = world.entity();
Entity e1 = world.entity();
Entity e2 = world.entity();
Entity e3 = world.entity();
SpatialMap collider;
collider.insert({11,11}, player);
collider.insert({10,10}, e2);
collider.insert({11,10}, e3);
collider.insert({21,21}, e1);
EntityList nearby = require_found(collider, {11,11}, false, 1);
REQUIRE(nearby[0] == e3);
nearby = require_found(collider, {11,11}, true, 2);
REQUIRE(nearby[0] == e3);
REQUIRE(nearby[1] == e2);
collider.move({11,11}, {20,20}, player);
nearby = require_found(collider, {20,20}, true, 1);
REQUIRE(nearby[0] == e1);
}
TEST_CASE("test edge cases that might crash", "[collision]") {
DinkyECS::World world;
Entity player = world.entity();
Entity enemy = world.entity();
SpatialMap collider;
collider.insert({0,0}, player);
Point enemy_at = {1, 0};
collider.insert(enemy_at, enemy);
EntityList nearby = require_found(collider, {0,0}, true, 1);
collider.move({1,0}, {1,1}, enemy);
nearby = require_found(collider, {0,0}, true, 1);
REQUIRE(nearby[0] == enemy);
collider.move({1,1}, {0,1}, enemy);
nearby = require_found(collider, {0,0}, true, 1);
REQUIRE(nearby[0] == enemy);
}
TEST_CASE("check all diagonal works", "[collision]") {
DinkyECS::World world;
Entity player = world.entity();
Entity enemy = world.entity();
SpatialMap collider;
Point player_at = {1,1};
collider.insert(player_at, player);
Point enemy_at = {1, 0};
collider.insert(enemy_at, enemy);
for(size_t x = 0; x <= 2; x++) {
for(size_t y = 0; y <= 2; y++) {
if(enemy_at.x == player_at.x && enemy_at.y == player_at.y) continue; // skip player spot
EntityList nearby = require_found(collider, player_at, true, 1);
REQUIRE(nearby[0] == enemy);
// move the enemy to a new spot around the player
Point move_to = {enemy_at.x + x, enemy_at.y + y};
collider.move(enemy_at, move_to, enemy);
enemy_at = move_to;
}
}
}

@ -0,0 +1,25 @@
#include <catch2/catch_test_macros.hpp>
#include <fmt/core.h>
#include "map.hpp"
#include "worldbuilder.hpp"
#include "tilemap.hpp"
#include "config.hpp"
#include "rand.hpp"
using namespace fmt;
using std::string;
TEST_CASE("tilemap can load tiles and make a map", "[tilemap]") {
size_t width = Random::uniform<size_t>(10, 25);
size_t height = Random::uniform<size_t>(10, 33);
Map map(width,height);
WorldBuilder builder(map);
builder.generate_map();
TileMap tiles(map.width(), map.height());
auto& walls = map.walls();
tiles.load(walls);
tiles.dump();
REQUIRE(tiles.INVARIANT());
}

@ -0,0 +1,35 @@
#include <catch2/catch_test_macros.hpp>
#include <fmt/core.h>
#include <nlohmann/json.hpp>
#include <fstream>
#include "map.hpp"
#include "worldbuilder.hpp"
using namespace fmt;
using namespace nlohmann;
using std::string;
TEST_CASE("bsp algo test", "[builder]") {
Map map(31, 20);
WorldBuilder builder(map);
builder.generate_map();
}
TEST_CASE("pathing", "[builder]") {
Map map(23, 14);
WorldBuilder builder(map);
builder.generate_map();
matrix::dump("WALLS", map.$walls, 0,0);
println("wall at 0,0=={}", map.$walls[0][0]);
for(matrix::each_cell it{map.$walls}; it.next();) {
if(map.$walls[it.y][it.x] == WALL_VALUE) {
REQUIRE(map.iswall(it.x, it.y) == true);
REQUIRE(map.can_move({it.x, it.y}) == false);
} else {
REQUIRE(map.iswall(it.x, it.y) == false);
REQUIRE(map.can_move({it.x, it.y}) == true);
}
}
}

@ -0,0 +1,75 @@
#include "tilemap.hpp"
#include "dbc.hpp"
#include "constants.hpp"
using nlohmann::json;
TileMap::TileMap(size_t width, size_t height) :
$config("./assets/tiles.json"),
$width(width),
$height(height),
$tile_ids(height, matrix::Row(width, SPACE_VALUE)),
$display(height, TileRow(width, {""}))
{
}
void TileMap::dump(int show_x, int show_y) {
for(matrix::each_row it{$tile_ids}; it.next();) {
const TileCell &cell = $display[it.y][it.x];
if(int(it.x) == show_x && int(it.y) == show_y) {
fmt::print("{}<", cell.display);
} else {
fmt::print("{} ", cell.display);
}
if(it.row) fmt::print("\n");
}
}
void TileMap::set_tile(size_t x, size_t y, string tile_name) {
std::wstring tile_id = $config.wstring(tile_name, "display");
json tile_conf = $config[tile_name];
TileCell tile{
tile_conf["display"],
tile_conf["foreground"][0],
tile_conf["foreground"][1],
tile_conf["foreground"][2],
tile_conf["background"][0],
tile_conf["background"][1],
tile_conf["background"][2]};
$tile_ids[y][x] = tile_id[0];
$display[y][x] = tile;
}
void TileMap::load(matrix::Matrix &walls) {
for(matrix::each_cell it{walls}; it.next();) {
string tile_name = walls[it.y][it.x] == SPACE_VALUE ? "FLOOR_TILE" : "WALL_TILE";
set_tile(it.x, it.y, tile_name);
}
}
const TileCell &TileMap::at(size_t x, size_t y) {
return $display[y][x];
}
std::vector<std::string> TileMap::tile_names(bool collision) {
const auto &json = $config.json();
std::vector<std::string> keys;
for(const auto& el : json.items()) {
const auto &val = el.value();
if(val["collision"] == collision) {
keys.push_back(el.key());
}
}
return keys;
}
bool TileMap::INVARIANT() {
dbc::check(matrix::height($tile_ids) == $height, "$tile_ids has wrong height");
dbc::check(matrix::width($tile_ids) == $width, "$tile_ids has wrong width");
return true;
}

@ -0,0 +1,46 @@
#pragma once
#include <vector>
#include <utility>
#include <string>
#include <algorithm>
#include <fmt/core.h>
#include "point.hpp"
#include "matrix.hpp"
#include "config.hpp"
struct TileCell {
std::string display;
uint8_t fg_h = 0;
uint8_t fg_s = 0;
uint8_t fg_v = 0;
uint8_t bg_h = 0;
uint8_t bg_s = 0;
uint8_t bg_v = 0;
};
typedef std::vector<TileCell> TileRow;
typedef std::vector<TileRow> TileGrid;
class TileMap {
public:
Config $config;
size_t $width;
size_t $height;
matrix::Matrix $tile_ids;
TileGrid $display;
TileMap(size_t width, size_t height);
// disable copying
TileMap(TileMap &map) = delete;
size_t width() { return $width; }
size_t height() { return $height; }
void load(matrix::Matrix &walls);
const TileCell &at(size_t x, size_t y);
void set_tile(size_t x, size_t y, std::string tile_name);
std::vector<std::string> tile_names(bool collision);
void dump(int show_x=-1, int show_y=-1);
bool INVARIANT();
};

@ -0,0 +1,220 @@
// Licensed under the Boost License <https://opensource.org/licenses/BSL-1.0>.
// SPDX-License-Identifier: BSL-1.0
#pragma once
#include <array>
#include <ostream>
#include <cstring>
#include <string>
#include <string_view>
#include <type_traits>
#include <tuple>
#include <locale>
#include <codecvt>
namespace tser{
//implementation details for C++20 is_detected
namespace detail {
struct ns {
~ns() = delete;
ns(ns const&) = delete;
};
template <class Default, class AlwaysVoid, template<class...> class Op, class... Args>
struct detector {
using value_t = std::false_type;
using type = Default;
};
template <class Default, template<class...> class Op, class... Args>
struct detector<Default, std::void_t<Op<Args...>>, Op, Args...> {
using value_t = std::true_type;
using type = Op<Args...>;
};
template<class T>
struct is_array : std::is_array<T> {};
template<template<typename, size_t> class TArray, typename T, size_t N>
struct is_array<TArray<T, N>> : std::true_type {};
constexpr size_t n_args(char const* c, size_t nargs = 1) {
for (; *c; ++c) if (*c == ',') ++nargs;
return nargs;
}
constexpr size_t str_size(char const* c, size_t strSize = 1) {
for (; *c; ++c) ++strSize;
return strSize;
}
}
// we need a bunch of template metaprogramming for being able to differentiate between different types
template <template<class...> class Op, class... Args>
constexpr bool is_detected_v = detail::detector<detail::ns, void, Op, Args...>::value_t::value;
class BinaryArchive;
template<class T> using has_begin_t = decltype(*std::begin(std::declval<T>()));
template<class T> using has_members_t = decltype(std::declval<T>().members());
template<class T> using has_smaller_t = decltype(std::declval<T>() < std::declval<T>());
template<class T> using has_equal_t = decltype(std::declval<T>() == std::declval<T>());
template<class T> using has_nequal_t = decltype(std::declval<T>() != std::declval<T>());
template<class T> using has_outstream_op_t = decltype(std::declval<std::ostream>() << std::declval<T>());
template<class T> using has_tuple_t = std::tuple_element_t<0, T>;
template<class T> using has_optional_t = decltype(std::declval<T>().has_value());
template<class T> using has_element_t = typename T::element_type;
template<class T> using has_mapped_t = typename T::mapped_type;
template<class T> using has_custom_save_t = decltype(std::declval<T>().save(std::declval<BinaryArchive&>()));
template<class T> using has_free_save_t = decltype(std::declval<const T&>() << std::declval<BinaryArchive&>());
template<class T> constexpr bool is_container_v = is_detected_v<has_begin_t, T>;
template<class T> constexpr bool is_tuple_v = is_detected_v<has_tuple_t, T>;
template<class T> constexpr bool is_tser_t_v = is_detected_v<has_members_t, T>;
template<class T> constexpr bool is_pointer_like_v = std::is_pointer_v<T> || is_detected_v<has_element_t, T> || is_detected_v<has_optional_t, T>;
class BinaryArchive {
std::string m_bytes = std::string(1024, '\0');
size_t m_bufferSize = 0, m_readOffset = 0;
public:
explicit BinaryArchive(const size_t initialSize = 1024) : m_bytes(initialSize, '\0') {}
template<typename T>
explicit BinaryArchive(const T& t) { save(t); }
template<typename T>
void save(const T& t) {
if constexpr (is_detected_v<has_free_save_t, T>) {
operator<<(t,*this);
} else if constexpr (is_detected_v<has_custom_save_t, T>) {
t.save(*this);
} else if constexpr(is_tser_t_v<T>) {
std::apply([&](auto& ... mVal) { (save(mVal), ...); }, t.members());
} else if constexpr(is_tuple_v<T>) {
std::apply([&](auto& ... tVal) { (save(tVal), ...); }, t);
} else if constexpr (is_pointer_like_v<T>) {
save(static_cast<bool>(t));
if (t)
save(*t);
} else if constexpr (is_container_v<T>) {
if constexpr (!detail::is_array<T>::value) {
save(t.size());
}
for (auto& val : t) save(val);
} else {
if (m_bufferSize + sizeof(T) + sizeof(T) / 4 > m_bytes.size()) {
m_bytes.resize((m_bufferSize + sizeof(T)) * 2);
}
std::memcpy(m_bytes.data() + m_bufferSize, std::addressof(t), sizeof(T));
m_bufferSize += sizeof(T);
}
}
template<typename T>
void load(T& t) {
using V = std::decay_t<T>;
if constexpr (is_detected_v<has_free_save_t, V>) {
operator>>(t, *this);
} else if constexpr (is_detected_v<has_custom_save_t, T>) {
t.load(*this);
} else if constexpr (is_tser_t_v<T>) {
std::apply([&](auto& ... mVal) { (load(mVal), ...); }, t.members());
} else if constexpr (is_tuple_v<V>) {
std::apply([&](auto& ... tVal) { (load(tVal), ...); }, t);
} else if constexpr (is_pointer_like_v<T>) {
if constexpr (std::is_pointer_v<T>) {
t = load<bool>() ? (t = new std::remove_pointer_t<T>(), load(*t), t) : nullptr;
} else if constexpr (is_detected_v<has_optional_t, T>) {
t = load<bool>() ? T(load<typename V::value_type>()) : T();
} else { //smart pointer
t = T(load<has_element_t<V>*>());
}
} else if constexpr (is_container_v<T>) {
if constexpr (!detail::is_array<T>::value) {
const auto size = load<decltype(t.size())>();
using VT = typename V::value_type;
for (size_t i = 0; i < size; ++i) {
if constexpr (!is_detected_v<has_mapped_t, V>) {
t.insert(t.end(), load<VT>());
} else {
//we have to special case map, because of the const key
t.emplace(VT{ load<typename V::key_type>(), load<typename V::mapped_type>() });
}
}
} else {
for (auto& val : t) load(val);
}
} else {
std::memcpy(&t, m_bytes.data() + m_readOffset, sizeof(T));
m_readOffset += sizeof(T);
}
}
template<typename T>
T load() {
std::remove_const_t<T> t{}; load(t); return t;
}
template<typename T>
friend BinaryArchive& operator<<(BinaryArchive& ba, const T& t) {
ba.save(t); return ba;
}
template<typename T>
friend BinaryArchive& operator>>(BinaryArchive& ba, T& t) {
ba.load(t); return ba;
}
void reset() {
m_bufferSize = 0;
m_readOffset = 0;
}
void initialize(std::string_view str) {
m_bytes = str;
m_bufferSize = str.size();
m_readOffset = 0;
}
std::string_view get_buffer() const {
return std::string_view(m_bytes.data(), m_bufferSize);
}
};
template<class Base, typename Derived>
std::conditional_t<std::is_const_v<Derived>, const Base, Base>& base(Derived* thisPtr) { return *thisPtr; }
template<typename T>
auto load(std::string_view encoded) { BinaryArchive ba(encoded); return ba.load<T>(); }
}
//this macro defines printing, serialisation and comparision operators (==,!=,<) for custom types
#define DEFINE_SERIALIZABLE(Type, ...) \
inline decltype(auto) members() const { return std::tie(__VA_ARGS__); } \
inline decltype(auto) members() { return std::tie(__VA_ARGS__); } \
static constexpr std::array<char, tser::detail::str_size(#__VA_ARGS__)> _memberNameData = [](){ \
std::array<char, tser::detail::str_size(#__VA_ARGS__)> chars{'\0'}; size_t _idx = 0; constexpr auto* ini(#__VA_ARGS__); \
for (char const* _c = ini; *_c; ++_c, ++_idx) { if(*_c != ',' && *_c != ' ') chars[_idx] = *_c; } return chars;}(); \
static constexpr const char* _typeName = #Type; \
static constexpr std::array<const char*, tser::detail::n_args(#__VA_ARGS__)> _memberNames = \
[](){ std::array<const char*, tser::detail::n_args(#__VA_ARGS__)> out{ }; \
for(size_t _i = 0, nArgs = 0; nArgs < tser::detail::n_args(#__VA_ARGS__) ; ++_i) { \
while(Type::_memberNameData[_i] == '\0') {_i++;} out[nArgs++] = &Type::_memberNameData[_i]; \
while(Type::_memberNameData[++_i] != '\0'); } return out;}();

@ -0,0 +1,347 @@
#include "worldbuilder.hpp"
#include "rand.hpp"
#include <fmt/core.h>
#include <iostream>
#include "components.hpp"
using namespace fmt;
using namespace components;
inline void check_player(DinkyECS::World &world, DinkyECS::Entity entity) {
auto player = world.get_the<Player>();
dbc::check(player.entity != entity, "player shouldn't be added to world");
auto tile = world.get<Tile>(player.entity);
dbc::check(tile.chr == "\ua66b", format("PLAYER TILE CHANGED {} != {}", tile.chr, "\ua66b"));
}
inline int make_split(Room &cur, bool horiz) {
size_t dimension = horiz ? cur.height : cur.width;
int min = dimension / WORLDBUILD_DIVISION;
int max = dimension - min;
return Random::uniform<int>(min, max);
}
void WorldBuilder::set_door(Room &room, int value) {
$map.$walls[room.entry.y][room.entry.x] = value;
$map.$walls[room.exit.y][room.exit.x] = value;
}
void rand_side(Room &room, Point &door) {
dbc::check(int(room.width) > 0 && int(room.height) > 0, "Weird room with 0 for height or width.");
int rand_x = Random::uniform<int>(0, room.width - 1);
int rand_y = Random::uniform<int>(0, room.height - 1);
switch(Random::uniform<int>(0,3)) {
case 0: // north
door.x = room.x + rand_x;
door.y = room.y-1;
break;
case 1: // south
door.x = room.x + rand_x;
door.y = room.y + room.height;
break;
case 2: // east
door.x = room.x + room.width;
door.y = room.y + rand_y;
break;
case 3: // west
door.x = room.x - 1;
door.y = room.y + rand_y;
break;
default:
dbc::sentinel("impossible side");
}
}
void WorldBuilder::add_door(Room &room) {
rand_side(room, room.entry);
rand_side(room, room.exit);
}
void WorldBuilder::partition_map(Room &cur, int depth) {
if(cur.width >= 3 && cur.width <= 6 &&
cur.height >= 3 && cur.height <= 6)
{
$map.add_room(cur);
return;
}
bool horiz = cur.width > cur.height ? false : true;
int split = make_split(cur, horiz);
if(split <= 0) return; // end recursion
Room left = cur;
Room right = cur;
if(horiz) {
if(split >= int(cur.height)) return; // end recursion
left.height = size_t(split - 1);
right.y = cur.y + split;
right.height = size_t(cur.height - split);
} else {
if(split >= int(cur.width)) return; // end recursion
left.width = size_t(split-1);
right.x = cur.x + split,
right.width = size_t(cur.width - split);
}
// BUG: min room size should be configurable
if(depth > 0 && left.width > 2 && left.height > 2) {
partition_map(left, depth-1);
}
// BUG: min room size should be configurable
if(depth > 0 && right.width > 2 && right.height > 2) {
partition_map(right, depth-1);
}
}
void WorldBuilder::update_door(Point &at, int wall_or_space) {
$map.$walls[at.y][at.x] = wall_or_space;
}
void WorldBuilder::stylize_room(int room, string tile_name, float size) {
Point pos_out;
bool placed = $map.place_entity(room, pos_out);
dbc::check(placed, "failed to place style in room");
for(matrix::circle it{$map.$walls, pos_out, size}; it.next();) {
for(int x = it.left; x < it.right; x++) {
if(!$map.iswall(x, it.y)) {
$map.$tiles.set_tile(x, it.y, tile_name);
}
}
}
}
void WorldBuilder::generate_rooms() {
Room root{
.x = 0,
.y = 0,
.width = $map.$width,
.height = $map.$height
};
// BUG: depth should be configurable
partition_map(root, 10);
place_rooms();
dbc::check($map.room_count() > 0, "map generated zero rooms, map too small?");
}
void WorldBuilder::generate_map() {
generate_rooms();
PointList holes;
for(size_t i = 0; i < $map.$rooms.size() - 1; i++) {
tunnel_doors(holes, $map.$rooms[i], $map.$rooms[i+1]);
}
// one last connection from first room to last
tunnel_doors(holes, $map.$rooms.back(), $map.$rooms.front());
// place all the holes
for(auto hole : holes) {
if(!matrix::inbounds($map.$walls, hole.x, hole.y)) {
matrix::dump("MAP BEFORE CRASH", $map.$walls, hole.x, hole.y);
auto err = fmt::format("invalid hold target {},{} map is only {},{}",
hole.x, hole.y, matrix::width($map.$walls),
matrix::height($map.$walls));
dbc::sentinel(err);
}
$map.$walls[hole.y][hole.x] = INV_SPACE;
}
$map.invert_space();
$map.expand();
$map.load_tiles();
// get only the tiles with no collision to fill rooms
auto room_types = $map.$tiles.tile_names(false);
for(size_t i = 0; i < $map.$rooms.size() - 1; i++) {
size_t room_type = Random::uniform<size_t>(0, room_types.size() - 1);
int room_size = Random::uniform<int>(100, 800);
string tile_name = room_types[room_type];
stylize_room(i, tile_name, room_size * 0.01f);
}
}
DinkyECS::Entity configure_entity_in_map(DinkyECS::World &world, Map &game_map, json &entity_data, int in_room) {
auto item = world.entity();
Point pos_out;
bool placed = game_map.place_entity(in_room, pos_out);
dbc::check(placed, "failed to randomly place item in room");
world.set<Position>(item, {pos_out.x+1, pos_out.y+1});
if(entity_data["inventory_count"] > 0) {
world.set<InventoryItem>(item, {entity_data["inventory_count"], entity_data});
}
if(entity_data.contains("components")) {
components::configure(world, item, entity_data);
}
return item;
}
inline json &select_entity_type(GameConfig &config, json &gen_config) {
int enemy_test = Random::uniform<int>(0,100);
int device_test = Random::uniform<int>(0, 100);
if(enemy_test < gen_config["enemy_probability"]) {
return config.enemies.json();
} else if(device_test < gen_config["device_probability"]) {
return config.devices.json();
} else {
return config.items.json();
}
}
void WorldBuilder::randomize_entities(DinkyECS::World &world, GameConfig &config) {
auto &gen_config = config.game["worldgen"];
for(size_t room_num = $map.room_count() - 1; room_num > 0; room_num--) {
int empty_room = Random::uniform<int>(0, 100);
if(empty_room < gen_config["empty_room_probability"]) continue;
json& entity_db = select_entity_type(config, gen_config);
std::vector<std::string> keys;
for(auto& el : entity_db.items()) {
auto& data = el.value();
if(data["placement"] == nullptr) {
keys.push_back(el.key());
}
}
int rand_entity = Random::uniform<int>(0, keys.size() - 1);
std::string key = keys[rand_entity];
auto entity_data = entity_db[key];
// pass that to the config as it'll be a generic json
auto entity = configure_entity_in_map(world, $map, entity_data, room_num);
check_player(world, entity);
}
}
inline void place_stairs(DinkyECS::World& world, GameConfig& config, Map& map) {
auto& device_config = config.devices.json();
auto entity_data = device_config["STAIRS_DOWN"];
int last_room = map.room_count() - 1;
auto entity = configure_entity_in_map(world, map, entity_data, last_room);
check_player(world, entity);
}
void WorldBuilder::place_entities(DinkyECS::World &world) {
auto &config = world.get_the<GameConfig>();
// configure a player as a fact of the world
if(world.has_the<Player>()) {
auto& player = world.get_the<Player>();
Point pos_out;
bool placed = $map.place_entity(0, pos_out);
dbc::check(placed, "failed to randomly place item in room");
world.set<Position>(player.entity, {pos_out.x+1, pos_out.y+1});
} else {
auto player_data = config.enemies["PLAYER_TILE"];
auto player_ent = configure_entity_in_map(world, $map, player_data, 0);
// configure player in the world
Player player{player_ent};
world.set_the<Player>(player);
world.set<Inventory>(player.entity, {5});
world.make_constant(player.entity);
}
randomize_entities(world, config);
place_stairs(world, config, $map);
}
void WorldBuilder::generate(DinkyECS::World &world) {
generate_map();
place_entities(world);
}
void WorldBuilder::make_room(size_t origin_x, size_t origin_y, size_t w, size_t h) {
$map.INVARIANT();
dbc::pre("y out of bounds", origin_y + h < $map.$height);
dbc::pre("x out of bounds", origin_x + w < $map.$width);
for(size_t y = origin_y; y < origin_y + h; ++y) {
for(size_t x = origin_x; x < origin_x + w; ++x) {
$map.$walls[y][x] = INV_SPACE;
}
}
}
void WorldBuilder::place_rooms() {
for(auto &cur : $map.$rooms) {
// println("ROOM x/y={},{}; w/h={},{}; map={},{}",
// cur.x, cur.y, cur.width, cur.height, $map.$width, $map.$height);
add_door(cur);
make_room(cur.x, cur.y, cur.width, cur.height);
}
}
inline bool random_path(Map &map, PointList &holes, Point src, Point target) {
bool keep_going = false;
bool target_found = false;
int count = 0;
map.set_target(target);
map.make_paths();
Matrix &paths = map.paths();
Point out{src.x, src.y};
do {
keep_going = map.neighbors(out, true);
holes.push_back(out);
target_found = paths[out.y][out.x] == 0;
} while(!target_found && keep_going && ++count < WORLDBUILD_MAX_PATH);
map.INVARIANT();
map.clear_target(target);
return target_found;
}
inline void straight_path(Map &map, PointList &holes, Point src, Point target) {
for(matrix::line dig{src, target}; dig.next();) {
holes.emplace_back(size_t(dig.x), size_t(dig.y));
Point expand{(size_t)dig.x+1, (size_t)dig.y};
if(map.inmap(expand.x, expand.y)) {
// BUG? should really just move doors away from the edge
holes.push_back(expand);
}
}
}
void WorldBuilder::tunnel_doors(PointList &holes, Room &src, Room &target) {
int path_type = Random::uniform<int>(0, 3);
switch(path_type) {
case 0:
// for now do 25% as simple straight paths
straight_path($map, holes, src.exit, target.entry);
break;
case 1:
// for now do 25% as simple straight paths
straight_path($map, holes, src.exit, target.entry);
break;
default:
// then do the rest as random with fallback
if(!random_path($map, holes, src.exit, target.entry)) {
straight_path($map, holes, src.exit, target.entry);
}
}
}

@ -0,0 +1,27 @@
#pragma once
#include "map.hpp"
#include "dinkyecs.hpp"
#include "components.hpp"
class WorldBuilder {
public:
Map& $map;
WorldBuilder(Map &map) : $map(map) { }
void partition_map(Room &cur, int depth);
void make_room(size_t origin_y, size_t origin_x, size_t width, size_t height);
void add_door(Room &room);
void set_door(Room &room, int value);
void place_rooms();
void tunnel_doors(PointList &holes, Room &src, Room &target);
void update_door(Point &at, int wall_or_space);
void stylize_room(int room, string tile_name, float size);
void generate_rooms();
void generate_map();
void place_entities(DinkyECS::World &world);
void generate(DinkyECS::World &world);
void random_entity(DinkyECS::World &world, components::GameConfig &config);
void randomize_entities(DinkyECS::World &world, components::GameConfig &config);
};
Loading…
Cancel
Save