From f01d551eb0850da0639eb33b47a217964a56047c Mon Sep 17 00:00:00 2001 From: ephphatha Date: Sat, 17 Jul 2021 10:19:56 +1000 Subject: [PATCH] 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. --- CMakeLists.txt | 1 + Source/engine/point.hpp | 16 ++++ test/path_test.cpp | 170 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 187 insertions(+) create mode 100644 test/path_test.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index f12ea44db..ce2e8556e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -531,6 +531,7 @@ if(RUN_TESTS) test/main.cpp test/missiles_test.cpp test/pack_test.cpp + test/path_test.cpp test/player_test.cpp test/quests_test.cpp test/random_test.cpp diff --git a/Source/engine/point.hpp b/Source/engine/point.hpp index f47fcde3c..8735e5636 100644 --- a/Source/engine/point.hpp +++ b/Source/engine/point.hpp @@ -1,6 +1,9 @@ #pragma once #include +#ifdef RUN_TESTS +#include +#endif #include "engine/direction.hpp" #include "engine/displacement.hpp" @@ -149,6 +152,19 @@ struct Point { return std::max(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 diff --git a/test/path_test.cpp b/test/path_test.cpp new file mode 100644 index 000000000..5795df709 --- /dev/null +++ b/test/path_test.cpp @@ -0,0 +1,170 @@ +#include + +#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 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