Browse Source

Add tests for path_get_h_cost, IsTileWalkable, IsTile(Not)Solid, path_solid_pieces, FindPath

Only path_solid_pieces and FindPath are used outside the file, but the other functions are exposed in the header and have behaviour I felt worth testing individually.
pull/2415/head
ephphatha 5 years ago committed by Anders Jenbo
parent
commit
f01d551eb0
  1. 1
      CMakeLists.txt
  2. 16
      Source/engine/point.hpp
  3. 170
      test/path_test.cpp

1
CMakeLists.txt

@ -531,6 +531,7 @@ if(RUN_TESTS)
test/main.cpp test/main.cpp
test/missiles_test.cpp test/missiles_test.cpp
test/pack_test.cpp test/pack_test.cpp
test/path_test.cpp
test/player_test.cpp test/player_test.cpp
test/quests_test.cpp test/quests_test.cpp
test/random_test.cpp test/random_test.cpp

16
Source/engine/point.hpp

@ -1,6 +1,9 @@
#pragma once #pragma once
#include <cmath> #include <cmath>
#ifdef RUN_TESTS
#include <ostream>
#endif
#include "engine/direction.hpp" #include "engine/direction.hpp"
#include "engine/displacement.hpp" #include "engine/displacement.hpp"
@ -149,6 +152,19 @@ struct Point {
return std::max<int>(offset.deltaX, offset.deltaY); return std::max<int>(offset.deltaX, offset.deltaY);
} }
#ifdef RUN_TESTS
/**
* @brief Format points nicely in test failure messages
* @param stream output stream, expected to have overloads for int and char*
* @param point Object to display
* @return the stream, to allow chaining
*/
friend std::ostream &operator<<(std::ostream &stream, const Point &point)
{
return stream << "(x: " << point.x << ", y: " << point.y << ")";
}
#endif
}; };
} // namespace devilution } // namespace devilution

170
test/path_test.cpp

@ -0,0 +1,170 @@
#include <gtest/gtest.h>
#include "path.h"
// The following headers are included to access globals used in functions that have not been isolated yet.
#include "gendung.h"
#include "objects.h"
namespace devilution {
TEST(PathTest, Heuristics)
{
constexpr Point source { 25, 32 };
Point destination = source;
EXPECT_EQ(path_get_h_cost(source.x, source.y, destination.x, destination.y), 0) << "Wrong cost for travelling to the same tile";
destination = source + Direction::DIR_NE;
EXPECT_EQ(path_get_h_cost(source.x, source.y, destination.x, destination.y), 2) << "Wrong cost for travelling to horizontal/vertical adjacent tile";
destination = source + Direction::DIR_SE;
EXPECT_EQ(path_get_h_cost(source.x, source.y, destination.x, destination.y), 2) << "Wrong cost for travelling to horizontal/vertical adjacent tile";
destination = source + Direction::DIR_SW;
EXPECT_EQ(path_get_h_cost(source.x, source.y, destination.x, destination.y), 2) << "Wrong cost for travelling to horizontal/vertical adjacent tile";
destination = source + Direction::DIR_NW;
EXPECT_EQ(path_get_h_cost(source.x, source.y, destination.x, destination.y), 2) << "Wrong cost for travelling to horizontal/vertical adjacent tile";
destination = source + Direction::DIR_N;
EXPECT_EQ(path_get_h_cost(source.x, source.y, destination.x, destination.y), 4) << "Wrong cost for travelling to diagonally adjacent tile";
destination = source + Direction::DIR_E;
EXPECT_EQ(path_get_h_cost(source.x, source.y, destination.x, destination.y), 4) << "Wrong cost for travelling to diagonally adjacent tile";
destination = source + Direction::DIR_S;
EXPECT_EQ(path_get_h_cost(source.x, source.y, destination.x, destination.y), 4) << "Wrong cost for travelling to diagonally adjacent tile";
destination = source + Direction::DIR_W;
EXPECT_EQ(path_get_h_cost(source.x, source.y, destination.x, destination.y), 4) << "Wrong cost for travelling to diagonally adjacent tile";
destination = source + Direction::DIR_SW + Direction::DIR_SE; // Effectively the same as DIR_S
EXPECT_EQ(path_get_h_cost(source.x, source.y, destination.x, destination.y), 4) << "Wrong cost for travelling to diagonally adjacent tile";
destination = source + Direction::DIR_NE + Direction::DIR_N;
EXPECT_EQ(path_get_h_cost(source.x, source.y, destination.x, destination.y), 6) << "Wrong cost for travelling to a { 2, 1 } offset";
destination = source + Direction::DIR_SE + Direction::DIR_SE;
EXPECT_EQ(path_get_h_cost(source.x, source.y, destination.x, destination.y), 4) << "Wrong cost for travelling to a { 2, 0 } offset";
}
TEST(PathTest, Solid)
{
dPiece[5][5] = 0;
nSolidTable[0] = true;
EXPECT_TRUE(IsTileSolid({ 5, 5 })) << "Solid in-bounds tiles are solid";
EXPECT_FALSE(IsTileNotSolid({ 5, 5 })) << "IsTileNotSolid returns the inverse of IsTileSolid for in-bounds tiles";
dPiece[6][6] = 1;
nSolidTable[1] = false;
EXPECT_FALSE(IsTileSolid({ 6, 6 })) << "Non-solid in-bounds tiles are not solid";
EXPECT_TRUE(IsTileNotSolid({ 6, 6 })) << "IsTileNotSolid returns the inverse of IsTileSolid for in-bounds tiles";
EXPECT_FALSE(IsTileSolid({ -1, 1 })) << "Out of bounds tiles are not solid"; // this reads out of bounds in the current code and may fail unexpectedly
EXPECT_FALSE(IsTileNotSolid({ -1, 1 })) << "Out of bounds tiles are also not not solid";
}
TEST(PathTest, SolidPieces)
{
PATHNODE node1 { 0, 0, 0, { 0, 0 } };
PATHNODE node2 { 0, 0, 0, { 0, 1 } };
PATHNODE node3 { 0, 0, 0, { 1, 0 } };
PATHNODE node4 { 0, 0, 0, { 1, 1 } };
dPiece[0][0] = 0;
dPiece[0][1] = 0;
dPiece[1][0] = 0;
dPiece[1][1] = 0;
nSolidTable[0] = false;
EXPECT_TRUE(path_solid_pieces(&node1, 1, 1)) << "A step in open space is free of solid pieces";
EXPECT_TRUE(path_solid_pieces(&node4, 0, 0)) << "A step in open space is free of solid pieces";
EXPECT_TRUE(path_solid_pieces(&node3, 0, 1)) << "A step in open space is free of solid pieces";
EXPECT_TRUE(path_solid_pieces(&node2, 1, 0)) << "A step in open space is free of solid pieces";
nSolidTable[1] = true;
dPiece[1][0] = 1;
EXPECT_TRUE(path_solid_pieces(&node2, 1, 0)) << "Can path to a destination which is solid";
EXPECT_TRUE(path_solid_pieces(&node3, 0, 1)) << "Can path from a starting position which is solid";
EXPECT_TRUE(path_solid_pieces(&node2, 1, 1)) << "Stepping in a cardinal direction ignores solid pieces";
EXPECT_TRUE(path_solid_pieces(&node3, 1, 1)) << "Stepping in a cardinal direction ignores solid pieces";
EXPECT_TRUE(path_solid_pieces(&node1, 1, 0)) << "Stepping in a cardinal direction ignores solid pieces";
EXPECT_TRUE(path_solid_pieces(&node4, 1, 0)) << "Stepping in a cardinal direction ignores solid pieces";
EXPECT_FALSE(path_solid_pieces(&node1, 1, 1)) << "Can't cut a solid corner";
EXPECT_FALSE(path_solid_pieces(&node4, 0, 0)) << "Can't cut a solid corner";
dPiece[0][1] = 1;
EXPECT_FALSE(path_solid_pieces(&node1, 1, 1)) << "Can't walk through the boundary between two corners";
EXPECT_FALSE(path_solid_pieces(&node4, 0, 0)) << "Can't walk through the boundary between two corners";
dPiece[1][0] = 0;
EXPECT_FALSE(path_solid_pieces(&node1, 1, 1)) << "Can't cut a solid corner";
EXPECT_FALSE(path_solid_pieces(&node4, 0, 0)) << "Can't cut a solid corner";
dPiece[0][1] = 0;
dPiece[0][0] = 1;
EXPECT_FALSE(path_solid_pieces(&node3, 0, 1)) << "Can't cut a solid corner";
EXPECT_FALSE(path_solid_pieces(&node2, 1, 0)) << "Can't cut a solid corner";
dPiece[1][1] = 1;
EXPECT_FALSE(path_solid_pieces(&node3, 0, 1)) << "Can't walk through the boundary between two corners";
EXPECT_FALSE(path_solid_pieces(&node2, 1, 0)) << "Can't walk through the boundary between two corners";
dPiece[0][0] = 0;
EXPECT_FALSE(path_solid_pieces(&node3, 0, 1)) << "Can't cut a solid corner";
EXPECT_FALSE(path_solid_pieces(&node2, 1, 0)) << "Can't cut a solid corner";
dPiece[1][1] = 0;
}
void CheckPath(Point startPosition, Point destinationPosition, std::vector<int8_t> expectedSteps)
{
static int8_t pathSteps[MAX_PATH_LENGTH];
auto pathLength = FindPath([](Point) { return true; }, startPosition.x, startPosition.y, destinationPosition.x, destinationPosition.y, pathSteps);
EXPECT_EQ(pathLength, expectedSteps.size()) << "Wrong path length for a path from " << startPosition << " to " << destinationPosition;
// Die early if the wrong path length is returned as we don't want to read oob in expectedSteps
ASSERT_LE(pathLength, expectedSteps.size()) << "Path is longer than expected.";
for (auto i = 0; i < pathLength; i++) {
EXPECT_EQ(pathSteps[i], expectedSteps[i]) << "Path step " << i << " differs from expectation for a path from "
<< startPosition << " to " << destinationPosition; // this shouldn't be a requirement but...
// Path directions are all jacked up compared to the Direction enum. Most consumers have their own mapping definition
//startPosition += Direction { path[i] - 1 };
}
// Given that we can't really make any assumptions about how the path is actually used.
//EXPECT_EQ(startPosition, destinationPosition) << "Path doesn't lead to destination";
}
TEST(PathTest, FindPath)
{
CheckPath({ 8, 8 }, { 8, 8 }, {});
// Traveling in cardinal directions is the only way to get a first step in a cardinal direction
CheckPath({ 8, 8 }, { 8, 6 }, { 1, 1 });
CheckPath({ 8, 8 }, { 6, 8 }, { 2, 2 });
CheckPath({ 8, 8 }, { 10, 8 }, { 3, 3 });
CheckPath({ 8, 8 }, { 8, 10 }, { 4, 4 });
// Otherwise pathing biases along diagonals and the diagonal steps will always be first
CheckPath({ 8, 8 }, { 5, 6 }, { 5, 5, 2 });
CheckPath({ 8, 8 }, { 4, 4 }, { 5, 5, 5, 5 });
CheckPath({ 8, 8 }, { 12, 20 }, { 7, 7, 7, 7, 4, 4, 4, 4, 4, 4, 4, 4 });
}
TEST(PathTest, Walkable)
{
dPiece[5][5] = 0;
nSolidTable[0] = true; // Doing this manually to save running through the code in gendung.cpp
EXPECT_FALSE(IsTileWalkable({ 5, 5 })) << "Tile which is marked as solid should be considered blocked";
EXPECT_FALSE(IsTileWalkable({ 5, 5 }, true)) << "Solid non-door tiles remain unwalkable when ignoring doors";
nSolidTable[0] = false;
EXPECT_TRUE(IsTileWalkable({ 5, 5 })) << "Non-solid tiles are walkable";
EXPECT_TRUE(IsTileWalkable({ 5, 5 }, true)) << "Non-solid tiles remain walkable when ignoring doors";
dObject[5][5] = 1;
Objects[0]._oSolidFlag = true;
EXPECT_FALSE(IsTileWalkable({ 5, 5 })) << "Tile occupied by a solid object is unwalkable";
EXPECT_FALSE(IsTileWalkable({ 5, 5 }, true)) << "Tile occupied by a solid non-door object are unwalkable when ignoring doors";
Objects[0]._otype = _object_id::OBJ_L1LDOOR;
EXPECT_FALSE(IsTileWalkable({ 5, 5 })) << "Tile occupied by a door which is marked as solid should be considered blocked";
EXPECT_TRUE(IsTileWalkable({ 5, 5 }, true)) << "Tile occupied by a door is considered walkable when ignoring doors";
Objects[0]._oSolidFlag = false;
EXPECT_TRUE(IsTileWalkable({ 5, 5 })) << "Tile occupied by an open door is walkable";
EXPECT_TRUE(IsTileWalkable({ 5, 5 }, true)) << "Tile occupied by a door is considered walkable when ignoring doors";
nSolidTable[0] = true;
EXPECT_FALSE(IsTileWalkable({ 5, 5 })) << "Solid tiles occupied by an open door remain unwalkable";
EXPECT_TRUE(IsTileWalkable({ 5, 5 }, true)) << "Solid tiles occupied by an open door become walkable when ignoring doors";
}
} // namespace devilution
Loading…
Cancel
Save