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

This commit is contained in:
Zed A. Shaw 2025-01-30 11:38:57 -05:00
parent 8d3d3b4ec3
commit 2daa1c9bd5
59 changed files with 4303 additions and 411 deletions

99
tests/components.cpp Normal file
View file

@ -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]") {
}

39
tests/dbc.cpp Normal file
View file

@ -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");
}
}

60
tests/dijkstra.json Normal file
View file

@ -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]
]
}
]

71
tests/inventory.cpp Normal file
View file

@ -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);
}

40
tests/levelmanager.cpp Normal file
View file

@ -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);
}

45
tests/lighting.cpp Normal file
View file

@ -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);
}

83
tests/map.cpp Normal file
View file

@ -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);
}
}

343
tests/matrix.cpp Normal file
View file

@ -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);
}
}

51
tests/pathing.cpp Normal file
View file

@ -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);
}

103
tests/save.cpp Normal file
View file

@ -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);
}

137
tests/spatialmap.cpp Normal file
View file

@ -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;
}
}
}

25
tests/tilemap.cpp Normal file
View file

@ -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());
}

35
tests/worldbuilder.cpp Normal file
View file

@ -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);
}
}
}