diff --git a/Makefile b/Makefile index a75c060..ca4ba17 100644 --- a/Makefile +++ b/Makefile @@ -37,7 +37,7 @@ tracy_build: meson compile -j 10 -C builddir test: asset_build build - ./builddir/runtests "[collision]" + ./builddir/runtests run: build test ifeq '$(OS)' 'Windows_NT' @@ -60,7 +60,7 @@ clean: meson compile --clean -C builddir debug_test: build - gdb --nx -x .gdbinit --ex run --args builddir/runtests -e "[collision]" + gdb --nx -x .gdbinit --ex run --args builddir/runtests -e win_installer: powershell 'start "C:\Program Files (x86)\solicus\InstallForge\bin\ifbuilderenvx86.exe" scripts\win_installer.ifp' diff --git a/spatialmap.cpp b/spatialmap.cpp index 224b9f0..2d2c877 100644 --- a/spatialmap.cpp +++ b/spatialmap.cpp @@ -6,7 +6,9 @@ using namespace fmt; using DinkyECS::Entity; void SpatialMap::insert(Point pos, Entity ent, bool has_collision) { - dbc::check(!occupied(pos), "attempt to insert an entity with collision in space with collision"); + if(has_collision) { + dbc::check(!occupied(pos), "attempt to insert an entity with collision in space with collision"); + } $collision.emplace(pos, CollisionData{ent, has_collision}); } diff --git a/tests/spatialmap.cpp b/tests/spatialmap.cpp index 5dcff59..4fc4bd0 100644 --- a/tests/spatialmap.cpp +++ b/tests/spatialmap.cpp @@ -4,171 +4,212 @@ #include "spatialmap.hpp" #include "dinkyecs.hpp" #include "rand.hpp" +#include 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]") { +TEST_CASE("SpatialMap::insert", "[spatialmap]") { DinkyECS::World world; - Entity player = world.entity(); - Entity enemy = world.entity(); - - SpatialMap collider; - collider.insert({11,11}, player, true); - collider.insert({21,21}, enemy, true); + SpatialMap map; - { // not found - auto [found, nearby] = collider.neighbors({1,1}); - REQUIRE(!found); - REQUIRE(nearby.empty()); - } + auto player = world.entity(); + auto item = world.entity(); + auto potion = world.entity(); + auto enemy = world.entity(); + Point at{10,10}; + Point enemy_at{11,11}; - // found - EntityList nearby = require_found(collider, {10,10}, true, 1); - REQUIRE(nearby[0] == player); + map.insert(at, item, false); + map.insert(at, potion, false); + REQUIRE(!map.occupied(at)); - { // removed - collider.remove({11,11}, player); - auto [found, nearby] = collider.neighbors({10,10}, true); - REQUIRE(!found); - REQUIRE(nearby.empty()); - } + map.insert(at, player, true); + REQUIRE(map.occupied(at)); - collider.insert({11,11}, player, true); // 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()); - } + REQUIRE_THROWS(map.insert(at, enemy, true)); - 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); + map.insert(enemy_at, enemy, true); + REQUIRE(map.occupied(enemy_at)); } - -TEST_CASE("confirm multiple entities moving", "[collision]") { +TEST_CASE("SpatialMap::remove", "[spatialmap]") { 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, true); - collider.insert({10,10}, e2, true); - collider.insert({11,10}, e3, true); - collider.insert({21,21}, e1, true); - - 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); -} + SpatialMap map; -TEST_CASE("test edge cases that might crash", "[collision]") { - DinkyECS::World world; - Entity player = world.entity(); - Entity enemy = world.entity(); + auto player = world.entity(); + auto item = world.entity(); + Point at{120, 120}; - SpatialMap collider; - collider.insert({0,0}, player, true); + // confirm that things can be in any order + map.insert(at, player, true); + map.insert(at, item, false); + REQUIRE(map.occupied(at)); - Point enemy_at = {1, 0}; - collider.insert(enemy_at, enemy, true); + auto data = map.remove(at, player); + REQUIRE(!map.occupied(at)); + REQUIRE(data.entity == player); + REQUIRE(data.collision == true); - EntityList nearby = require_found(collider, {0,0}, true, 1); + REQUIRE_THROWS(map.remove(at, player)); +} - 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("SpatialMap::move", "[spatialmap]") { + DinkyECS::World world; + SpatialMap map; + + auto player = world.entity(); + auto item = world.entity(); + Point at{10, 320}; + map.insert(at, player, true); + map.insert(at, item, false); + REQUIRE(map.occupied(at)); + + auto enemy = world.entity(); + auto potion = world.entity(); + Point enemy_at{11, 320}; + map.insert(enemy_at, enemy, true); + map.insert(enemy_at, potion, false); + REQUIRE(map.occupied(enemy_at)); + + Point target{at.x + 1, at.y}; + + // try bad move with a slot that's empty + REQUIRE_THROWS(map.move({0,0}, target, player)); + + // try move into an occupied spot also fails + REQUIRE_THROWS(map.move(at, target, player)); + + // now move to a new spot, need to add them back + map.insert(at, player, true); + target.x++; // just move farther + map.move(at, target, player); + + REQUIRE(map.occupied(target)); + auto data = map.remove(target, player); + REQUIRE(data.entity == player); + REQUIRE(data.collision == true); } -TEST_CASE("check all diagonal works", "[collision]") { + +TEST_CASE("SpatialMap::occupied/something_there", "[spatialmap]") { DinkyECS::World world; - Entity player = world.entity(); - Entity enemy = world.entity(); + SpatialMap map; + + auto player = world.entity(); + auto item = world.entity(); + + Point at{1000, 20}; + // first test empty locations + REQUIRE(!map.something_there(at)); + REQUIRE(!map.occupied(at)); + + // then when there's something without collision + map.insert(at, item, false); + REQUIRE(map.something_there(at)); + REQUIRE(!map.occupied(at)); + + // finally with collision and an item there + map.insert(at, player, true); + REQUIRE(map.something_there(at)); + REQUIRE(map.occupied(at)); + + // then remove the item and still have collision + + map.remove(at, item); + REQUIRE(map.something_there(at)); + REQUIRE(map.occupied(at)); + + // remove player and back to no collision + map.remove(at, player); + REQUIRE(!map.something_there(at)); + REQUIRE(!map.occupied(at)); + + // last thing, put just the player in at a new spot + Point target{at.x+1, at.y+10}; + map.insert(target, player, true); + REQUIRE(map.something_there(target)); + REQUIRE(map.occupied(target)); +} - SpatialMap collider; - Point player_at = {1,1}; - collider.insert(player_at, player, true); - Point enemy_at = {1, 0}; - collider.insert(enemy_at, enemy, true); +TEST_CASE("SpatialMap::get", "[spatialmap]") { + DinkyECS::World world; + SpatialMap map; - 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); + auto player = world.entity(); + auto item = world.entity(); + Point at{101, 31}; - // move the enemy to a new spot around the player - Point move_to = {enemy_at.x + x, enemy_at.y + y}; + // finally with collision and an item there + map.insert(at, player, true); + REQUIRE(map.occupied(at)); - if(!collider.occupied(move_to)) { - collider.move(enemy_at, move_to, enemy); - } + auto entity = map.get(at); + REQUIRE(player == entity); - enemy_at = move_to; - } - } + // This probably doesn't work so need to + // rethink how get works. + map.insert(at, item, false); + entity = map.get(at); + REQUIRE(entity == item); } -TEST_CASE("confirm can iterate through all", "[collision]") { +TEST_CASE("SpatialMap::neighbors", "[spatialmap]") { DinkyECS::World world; - SpatialMap collider; - Point player{10,10}; + SpatialMap map; - for(int i = 0; i < 10; i++) { - size_t max = Random::uniform(2,30); + auto player = world.entity(); + auto enemy1 = world.entity(); + auto enemy2 = world.entity(); + //auto item1 = world.entity(); + //auto item2 = world.entity(); + Point at{101, 31}; - for(size_t i = 0; i < max; i++) { - size_t x = Random::uniform(0, 213); - size_t y = Random::uniform(0, 251); + map.insert(at, player, true); + map.insert({at.x+1, at.y}, enemy1, true); + map.insert({at.x-1, at.y+1}, enemy2, true); - Entity ent = world.entity(); - if(!collider.occupied({x, y})) { - collider.insert({x,y}, ent, true); - } - } + auto result = map.neighbors(at, true); + REQUIRE(result.found); + REQUIRE(result.nearby.size() > 0); - auto sprite_distance = collider.distance_sorted(player, 1000); + bool maybe = result.nearby[0] == enemy1 || result.nearby[1] == enemy1; + REQUIRE(maybe); - // sometimes there's nothing near us - if(sprite_distance.size() == 0) continue; + maybe = result.nearby[0] == enemy2 || result.nearby[1] == enemy2; + REQUIRE(maybe); - int prev_dist = sprite_distance[0].first; + result = map.neighbors(at, false); + REQUIRE(result.found); + REQUIRE(result.nearby.size() == 1); + REQUIRE(result.nearby[0] == enemy1); +} - for(auto dist : sprite_distance) { - REQUIRE(prev_dist >= dist.first); - prev_dist = dist.first; - } +TEST_CASE("SpatialMap::distance_sorted", "[spatialmap]") { + + DinkyECS::World world; + SpatialMap map; + + auto player = world.entity(); + auto enemy1 = world.entity(); + auto item = world.entity(); + + map.insert({1,1}, player, true); + map.insert({4,4}, enemy1, true); + map.insert({3, 3}, item, false); + + auto result = map.distance_sorted({1, 1}, 100); + REQUIRE(result.size() == 3); + REQUIRE(result[0].second == enemy1); + REQUIRE(result[1].second == item); + REQUIRE(result[2].second == player); + + int prev_dist = std::numeric_limits::max(); + for(auto [dist, entity] : result) { + REQUIRE(dist < prev_dist); + prev_dist = dist; } }