SpatialMap now has a full test suite and that helped find some bugs.

master
Zed A. Shaw 3 days ago
parent f26189c696
commit 23ead1f0ca
  1. 4
      Makefile
  2. 2
      spatialmap.cpp
  3. 295
      tests/spatialmap.cpp

@ -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'

@ -6,7 +6,9 @@ using namespace fmt;
using DinkyECS::Entity;
void SpatialMap::insert(Point pos, Entity ent, bool has_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});
}

@ -4,171 +4,212 @@
#include "spatialmap.hpp"
#include "dinkyecs.hpp"
#include "rand.hpp"
#include <limits>
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 map;
SpatialMap collider;
collider.insert({11,11}, player, true);
collider.insert({21,21}, enemy, true);
{ // 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());
}
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());
}
map.insert(at, player, true);
REQUIRE(map.occupied(at));
nearby = require_found(collider, {11,11}, true, 1);
REQUIRE(nearby[0] == player);
REQUIRE_THROWS(map.insert(at, enemy, true));
// 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<size_t>(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<size_t>(0, 213);
size_t y = Random::uniform<size_t>(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);
bool maybe = result.nearby[0] == enemy1 || result.nearby[1] == enemy1;
REQUIRE(maybe);
auto sprite_distance = collider.distance_sorted(player, 1000);
maybe = result.nearby[0] == enemy2 || result.nearby[1] == enemy2;
REQUIRE(maybe);
// sometimes there's nothing near us
if(sprite_distance.size() == 0) continue;
result = map.neighbors(at, false);
REQUIRE(result.found);
REQUIRE(result.nearby.size() == 1);
REQUIRE(result.nearby[0] == enemy1);
}
int prev_dist = sprite_distance[0].first;
TEST_CASE("SpatialMap::distance_sorted", "[spatialmap]") {
for(auto dist : sprite_distance) {
REQUIRE(prev_dist >= dist.first);
prev_dist = dist.first;
}
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<int>::max();
for(auto [dist, entity] : result) {
REQUIRE(dist < prev_dist);
prev_dist = dist;
}
}

Loading…
Cancel
Save