diff --git a/test/tests/Pathfinding.cpp b/test/tests/Pathfinding.cpp new file mode 100644 index 0000000000..16d4bfe4f7 --- /dev/null +++ b/test/tests/Pathfinding.cpp @@ -0,0 +1,249 @@ +#include "TestData.h" +#include "openrct2/core/StringReader.hpp" +#include "openrct2/peep/Peep.h" +#include "openrct2/scenario/Scenario.h" + +#include +#include +#include +#include +#include +#include +#include + +using namespace OpenRCT2; + +static std::ostream& operator<<(std::ostream& os, const TileCoordsXYZ& coords) +{ + return os << "(" << coords.x << ", " << coords.y << ", " << coords.z << ")"; +} + +class PathfindingTestBase : public testing::Test +{ +public: + static void SetUpTestCase() + { + gOpenRCT2Headless = true; + gOpenRCT2NoGraphics = true; + _context = CreateContext(); + const bool initialised = _context->Initialise(); + ASSERT_TRUE(initialised); + + std::string parkPath = TestData::GetParkPath("pathfinding-tests.sv6"); + load_from_sv6(parkPath.c_str()); + game_load_init(); + + bitcount_init(); + } + + void SetUp() override + { + // Use a consistent random seed in every test + gScenarioSrand0 = 0x12345678; + gScenarioSrand1 = 0x87654321; + } + + static void TearDownTestCase() + { + _context = nullptr; + } + +protected: + + static bool FindPath(TileCoordsXYZ* pos, const TileCoordsXYZ& goal, int maxSteps) + { + // Our start position is in tile coordinates, but we need to give the peep spawn + // position in actual world coords (32 units per tile X/Y, 8 per Z level). + // Add 16 so the peep spawns in the center of the tile. + rct_peep* peep = peep_generate(pos->x * 32 + 16, pos->y * 32 + 16, pos->z * 8); + + // Peeps that are outside of the park use specialized pathfinding which we don't want to + // use here + peep->outside_of_park = 0; + + // Pick the direction the peep should initially move in, given the goal position. + // This will also store the goal position and initialize pathfinding data for the peep. + gPeepPathFindGoalPosition = goal; + const int32_t moveDir = peep_pathfind_choose_direction(*pos, peep); + if (moveDir < 0) + { + // Couldn't determine a direction to move off in + return false; + } + + // We have already set up the peep's overall pathfinding goal, but we also have to set their initial + // 'destination' which is a close position that they will walk towards in a straight line - in this case, one + // tile away. Stepping the peep will move them towards their destination, and once they reach it, a new + // destination will be picked, to try and get the peep towards the overall pathfinding goal. + peep->direction = moveDir; + peep->destination_x = peep->x + CoordsDirectionDelta[moveDir].x; + peep->destination_y = peep->y + CoordsDirectionDelta[moveDir].y; + peep->destination_tolerance = 2; + + // Repeatedly step the peep, until they reach the target position or until the expected number of steps have + // elapsed. Each step, check that the tile they are standing on is not marked as forbidden in the test data + // (red neon ground type). + int step = 0; + while (!(*pos == goal) && step < maxSteps) + { + uint8_t pathingResult = 0; + peep->PerformNextAction(pathingResult); + ++step; + + pos->x = peep->x / 32; + pos->y = peep->y / 32; + pos->z = peep->z / 8; + + EXPECT_PRED_FORMAT1(AssertIsNotForbiddenPosition, *pos); + } + + // Clean up the peep, because we're reusing this loaded context for all tests. + peep_sprite_remove(peep); + + // Require that the number of steps taken is exactly what we expected. The pathfinder is supposed to be + // deterministic, and we reset the RNG seed for each test, everything should be entirely repeatable; as + // such a change in the number of steps taken on one of these paths needs to be reviewed. + EXPECT_EQ(step, maxSteps); + + return *pos == goal; + } + + static ::testing::AssertionResult AssertIsStartPosition(const char*, const TileCoordsXYZ& location) + { + return AssertPositionIsSetUp("Start", 11u, location); + } + + static ::testing::AssertionResult AssertIsGoalPosition(const char*, const TileCoordsXYZ& location) + { + return AssertPositionIsSetUp("Goal", 9u, location); + } + + static ::testing::AssertionResult AssertIsNotForbiddenPosition(const char*, const TileCoordsXYZ& location) + { + const uint32_t forbiddenSurfaceStyle = 8u; + + const uint32_t style = GetSurfaceStyleAtLocation(TileCoordsXY(location.x, location.y)); + + if (style == forbiddenSurfaceStyle) + return ::testing::AssertionFailure() + << "Path traversed location " << location << ", but it is marked as a forbidden location (surface style " + << forbiddenSurfaceStyle << "). Either the map is set up incorrectly, or the pathfinder went the wrong way."; + + return ::testing::AssertionSuccess(); + } + +private: + + static uint32_t GetSurfaceStyleAtLocation(const TileCoordsXY& location) + { + TileElement* element = map_get_first_element_at(location.x, location.y); + + // Every tile *should* have a surface sprite, so we should be guaranteed to find + // something before we go off the end of the data. + while (element->GetType() != TILE_ELEMENT_TYPE_SURFACE) + element++; + + return element->AsSurface()->GetSurfaceStyle(); + } + + static ::testing::AssertionResult AssertPositionIsSetUp(const char* positionKind, uint32_t expectedSurfaceStyle, const TileCoordsXYZ& location) + { + const uint32_t style = GetSurfaceStyleAtLocation(TileCoordsXY(location.x, location.y)); + + if (style != expectedSurfaceStyle) + return ::testing::AssertionFailure() + << positionKind << " location " << location << " should have surface style " << expectedSurfaceStyle + << " but actually has style " << style + << ". Either the test map is not set up correctly, or you got the coordinates wrong."; + + return ::testing::AssertionSuccess(); + } + + static std::shared_ptr _context; +}; + +std::shared_ptr PathfindingTestBase::_context; + +struct SimplePathfindingScenario +{ + const char* name; + TileCoordsXYZ start; + TileCoordsXYZ goal; + uint32_t steps; + + SimplePathfindingScenario(const char* _name, const TileCoordsXYZ& _start, const TileCoordsXYZ& _goal, int _steps = 10000) + : name(_name) + , start(_start) + , goal(_goal) + , steps(_steps) + { + } + + friend std::ostream& operator<<(std::ostream& os, const SimplePathfindingScenario& scenario) + { + return os << scenario.start << " => " << scenario.goal; + } + + static std::string ToName(const ::testing::TestParamInfo& param_info) + { + return param_info.param.name; + } +}; + +class SimplePathfindingTest : public PathfindingTestBase, public ::testing::WithParamInterface +{ +}; + +TEST_P(SimplePathfindingTest, CanFindPathFromStartToGoal) +{ + const SimplePathfindingScenario& scenario = GetParam(); + + ASSERT_PRED_FORMAT1(AssertIsStartPosition, scenario.start); + ASSERT_PRED_FORMAT1(AssertIsGoalPosition, scenario.goal); + + TileCoordsXYZ pos = scenario.start; + + const auto succeeded = FindPath(&pos, scenario.goal, scenario.steps) ? ::testing::AssertionSuccess() + : ::testing::AssertionFailure() + << "Failed to find path from " << scenario.start << " to " << scenario.goal << " in " << scenario.steps + << " steps; reached " << pos << " before giving up."; + + EXPECT_TRUE(succeeded); +} + +INSTANTIATE_TEST_CASE_P( + ForScenario, SimplePathfindingTest, + ::testing::Values( + SimplePathfindingScenario("StraightFlat", { 2, 19, 14 }, { 4, 19, 14 }, 24), + SimplePathfindingScenario("SBend", { 2, 17, 14 }, { 4, 16, 14 }, 39), + SimplePathfindingScenario("UBend", { 2, 14, 14 }, { 2, 12, 14 }, 88), + SimplePathfindingScenario("CBend", { 2, 10, 14 }, { 2, 7, 14 }, 133), + SimplePathfindingScenario("TwoEqualRoutes", { 6, 18, 14 }, { 10, 18, 14 }, 819), + SimplePathfindingScenario("TwoUnequalRoutes", { 6, 14, 14 }, { 10, 14, 14 }, 15643), + SimplePathfindingScenario("StraightUpBridge", { 2, 4, 14 }, { 4, 4, 16 }, 24), + SimplePathfindingScenario("StraightUpSlope", { 4, 1, 14 }, { 6, 1, 16 }, 24), + SimplePathfindingScenario("SelfCrossingPath", { 6, 5, 14 }, { 8, 5, 14 }, 213)), + SimplePathfindingScenario::ToName); + +class ImpossiblePathfindingTest : public PathfindingTestBase, public ::testing::WithParamInterface +{ +}; + +TEST_P(ImpossiblePathfindingTest, CannotFindPathFromStartToGoal) +{ + const SimplePathfindingScenario& scenario = GetParam(); + TileCoordsXYZ pos = scenario.start; + + ASSERT_PRED_FORMAT1(AssertIsStartPosition, scenario.start); + ASSERT_PRED_FORMAT1(AssertIsGoalPosition, scenario.goal); + + EXPECT_FALSE(FindPath(&pos, scenario.goal, 10000)); +} + +INSTANTIATE_TEST_CASE_P( + ForScenario, ImpossiblePathfindingTest, + ::testing::Values( + SimplePathfindingScenario("PathWithGap", { 6, 9, 14 }, { 10, 9, 14 }), + SimplePathfindingScenario("PathWithFences", { 6, 7, 14 }, { 10, 7, 14 }), + SimplePathfindingScenario("PathWithCliff", { 10, 5, 14 }, { 12, 5, 14 })), + SimplePathfindingScenario::ToName); diff --git a/test/tests/testdata/parks/pathfinding-tests.sv6 b/test/tests/testdata/parks/pathfinding-tests.sv6 new file mode 100644 index 0000000000..41c0ab0345 Binary files /dev/null and b/test/tests/testdata/parks/pathfinding-tests.sv6 differ diff --git a/test/tests/tests.vcxproj b/test/tests/tests.vcxproj index 3d194ddff4..adddb17b74 100644 --- a/test/tests/tests.vcxproj +++ b/test/tests/tests.vcxproj @@ -1,4 +1,4 @@ - + ..\..\ @@ -64,6 +64,7 @@ +