#include "TestData.h" #include "openrct2/core/StringReader.h" #include "openrct2/peep/GuestPathfinding.h" #include "openrct2/peep/Peep.h" #include "openrct2/ride/Station.h" #include "openrct2/scenario/Scenario.h" #include #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() { core_init(); 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(); } void SetUp() override { // Use a consistent random seed in every test scenario_rand_seed(0x12345678, 0x87654321); } static void TearDownTestCase() { _context = nullptr; } protected: static Ride* FindRideByName(const char* name) { for (auto& ride : GetRideManager()) { auto thisName = ride.GetName(); if (!_strnicmp(thisName.c_str(), name, sizeof(thisName))) { return &ride; } } return nullptr; } static bool FindPath(TileCoordsXYZ* pos, const TileCoordsXYZ& goal, int expectedSteps, ride_id_t targetRideID) { // 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 centre of the tile. auto* peep = Guest::Generate(pos->ToCoordsXYZ().ToTileCentre()); // Peeps that are outside of the park use specialized pathfinding which we don't want to // use here peep->OutsideOfPark = false; // An earlier iteration of this code just gave peeps a target position to walk to, but it turns out // that with no actual ride to head towards, when a peep reaches a junction they use the 'aimless' // pathfinder instead of pursuing their original pathfinding target. So, we always need to give them // an actual ride to walk to the entrance of. peep->GuestHeadingToRideId = targetRideID; // 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 Direction moveDir = peep_pathfind_choose_direction(*pos, peep); if (moveDir == INVALID_DIRECTION) { // 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->PeepDirection = moveDir; auto destination = CoordsDirectionDelta[moveDir] + peep->GetLocation(); peep->SetDestination(destination, 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 < expectedSteps) { uint8_t pathingResult = 0; peep->PerformNextAction(pathingResult); ++step; *pos = TileCoordsXYZ(peep->GetLocation()); EXPECT_PRED_FORMAT1(AssertIsNotForbiddenPosition, *pos); // Check that the peep is still on a footpath. Use next_z instead of pos->z here because pos->z will change // when the peep is halfway up a slope, but next_z will not change until they move to the next tile. EXPECT_NE(map_get_footpath_element({ pos->ToCoordsXY(), peep->NextLoc.z }), nullptr); } // 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. For the negative // tests, we will not have reached the goal but we still expect the loop to have run for the total number // of steps requested before giving up. EXPECT_EQ(step, expectedSteps); return *pos == goal; } static ::testing::AssertionResult AssertIsStartPosition(const char*, const TileCoordsXYZ& location) { const uint32_t expectedSurfaceStyle = 11u; const uint32_t style = map_get_surface_element_at(location.ToCoordsXYZ())->GetSurfaceStyle(); if (style != expectedSurfaceStyle) return ::testing::AssertionFailure() << "Start 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 ::testing::AssertionResult AssertIsNotForbiddenPosition(const char*, const TileCoordsXYZ& location) { const uint32_t forbiddenSurfaceStyle = 8u; const uint32_t style = map_get_surface_element_at(location.ToCoordsXYZ())->GetSurfaceStyle(); 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 std::shared_ptr _context; }; std::shared_ptr PathfindingTestBase::_context; struct SimplePathfindingScenario { const char* name; TileCoordsXYZ start; uint32_t steps; SimplePathfindingScenario(const char* _name, const TileCoordsXYZ& _start, int _steps) : name(_name) , start(_start) , steps(_steps) { } 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); TileCoordsXYZ pos = scenario.start; auto ride = FindRideByName(scenario.name); ASSERT_NE(ride, nullptr); auto entrancePos = ride_get_entrance_location(ride, 0); TileCoordsXYZ goal = TileCoordsXYZ( entrancePos.x - TileDirectionDelta[entrancePos.direction].x, entrancePos.y - TileDirectionDelta[entrancePos.direction].y, entrancePos.z); const auto succeeded = FindPath(&pos, goal, scenario.steps, ride->id) ? ::testing::AssertionSuccess() : ::testing::AssertionFailure() << "Failed to find path from " << scenario.start << " to " << goal << " in " << scenario.steps << " steps; reached " << pos << " before giving up."; EXPECT_TRUE(succeeded); } INSTANTIATE_TEST_CASE_P( ForScenario, SimplePathfindingTest, ::testing::Values( SimplePathfindingScenario("StraightFlat", { 19, 15, 14 }, 24), SimplePathfindingScenario("SBend", { 15, 12, 14 }, 87), SimplePathfindingScenario("UBend", { 17, 9, 14 }, 87), SimplePathfindingScenario("CBend", { 14, 5, 14 }, 164), SimplePathfindingScenario("TwoEqualRoutes", { 9, 13, 14 }, 89), SimplePathfindingScenario("TwoUnequalRoutes", { 3, 13, 14 }, 89), SimplePathfindingScenario("StraightUpBridge", { 12, 15, 14 }, 24), SimplePathfindingScenario("StraightUpSlope", { 14, 15, 14 }, 24), SimplePathfindingScenario("SelfCrossingPath", { 6, 5, 14 }, 211)), 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); auto ride = FindRideByName(scenario.name); ASSERT_NE(ride, nullptr); auto entrancePos = ride_get_entrance_location(ride, 0); TileCoordsXYZ goal = TileCoordsXYZ( entrancePos.x + TileDirectionDelta[entrancePos.direction].x, entrancePos.y + TileDirectionDelta[entrancePos.direction].y, entrancePos.z); EXPECT_FALSE(FindPath(&pos, goal, 10000, ride->id)); } INSTANTIATE_TEST_CASE_P( ForScenario, ImpossiblePathfindingTest, ::testing::Values( SimplePathfindingScenario("PathWithGap", { 1, 6, 14 }, 10000), SimplePathfindingScenario("PathWithFences", { 11, 6, 14 }, 10000), SimplePathfindingScenario("PathWithCliff", { 7, 17, 14 }, 10000)), SimplePathfindingScenario::ToName);