1 #include "TestData.h"
2 #include "openrct2/core/StringReader.h"
3 #include "openrct2/peep/Guest.h"
4 #include "openrct2/peep/GuestPathfinding.h"
5 #include "openrct2/ride/Station.h"
6 #include "openrct2/scenario/Scenario.h"
7 
8 #include <gtest/gtest.h>
9 #include <openrct2/Context.h>
10 #include <openrct2/Game.h>
11 #include <openrct2/OpenRCT2.h>
12 #include <openrct2/ParkImporter.h>
13 #include <openrct2/platform/platform.h>
14 #include <openrct2/world/Footpath.h>
15 #include <openrct2/world/Map.h>
16 
17 using namespace OpenRCT2;
18 
operator <<(std::ostream & os,const TileCoordsXYZ & coords)19 static std::ostream& operator<<(std::ostream& os, const TileCoordsXYZ& coords)
20 {
21     return os << "(" << coords.x << ", " << coords.y << ", " << coords.z << ")";
22 }
23 
24 class PathfindingTestBase : public testing::Test
25 {
26 public:
SetUpTestCase()27     static void SetUpTestCase()
28     {
29         core_init();
30 
31         gOpenRCT2Headless = true;
32         gOpenRCT2NoGraphics = true;
33         _context = CreateContext();
34         const bool initialised = _context->Initialise();
35         ASSERT_TRUE(initialised);
36 
37         std::string parkPath = TestData::GetParkPath("pathfinding-tests.sv6");
38         load_from_sv6(parkPath.c_str());
39         game_load_init();
40     }
41 
SetUp()42     void SetUp() override
43     {
44         // Use a consistent random seed in every test
45         scenario_rand_seed(0x12345678, 0x87654321);
46     }
47 
TearDownTestCase()48     static void TearDownTestCase()
49     {
50         _context = nullptr;
51     }
52 
53 protected:
FindRideByName(const char * name)54     static Ride* FindRideByName(const char* name)
55     {
56         for (auto& ride : GetRideManager())
57         {
58             auto thisName = ride.GetName();
59             if (!_strnicmp(thisName.c_str(), name, sizeof(thisName)))
60             {
61                 return &ride;
62             }
63         }
64         return nullptr;
65     }
66 
FindPath(TileCoordsXYZ * pos,const TileCoordsXYZ & goal,int expectedSteps,ride_id_t targetRideID)67     static bool FindPath(TileCoordsXYZ* pos, const TileCoordsXYZ& goal, int expectedSteps, ride_id_t targetRideID)
68     {
69         // Our start position is in tile coordinates, but we need to give the peep spawn
70         // position in actual world coords (32 units per tile X/Y, 8 per Z level).
71         // Add 16 so the peep spawns in the centre of the tile.
72         auto* peep = Guest::Generate(pos->ToCoordsXYZ().ToTileCentre());
73 
74         // Peeps that are outside of the park use specialized pathfinding which we don't want to
75         // use here
76         peep->OutsideOfPark = false;
77 
78         // An earlier iteration of this code just gave peeps a target position to walk to, but it turns out
79         // that with no actual ride to head towards, when a peep reaches a junction they use the 'aimless'
80         // pathfinder instead of pursuing their original pathfinding target. So, we always need to give them
81         // an actual ride to walk to the entrance of.
82         peep->GuestHeadingToRideId = targetRideID;
83 
84         // Pick the direction the peep should initially move in, given the goal position.
85         // This will also store the goal position and initialize pathfinding data for the peep.
86         gPeepPathFindGoalPosition = goal;
87         const Direction moveDir = peep_pathfind_choose_direction(*pos, peep);
88         if (moveDir == INVALID_DIRECTION)
89         {
90             // Couldn't determine a direction to move off in
91             return false;
92         }
93 
94         // We have already set up the peep's overall pathfinding goal, but we also have to set their initial
95         // 'destination' which is a close position that they will walk towards in a straight line - in this case, one
96         // tile away. Stepping the peep will move them towards their destination, and once they reach it, a new
97         // destination will be picked, to try and get the peep towards the overall pathfinding goal.
98         peep->PeepDirection = moveDir;
99         auto destination = CoordsDirectionDelta[moveDir] + peep->GetLocation();
100         peep->SetDestination(destination, 2);
101 
102         // Repeatedly step the peep, until they reach the target position or until the expected number of steps have
103         // elapsed. Each step, check that the tile they are standing on is not marked as forbidden in the test data
104         // (red neon ground type).
105         int step = 0;
106         while (!(*pos == goal) && step < expectedSteps)
107         {
108             uint8_t pathingResult = 0;
109             peep->PerformNextAction(pathingResult);
110             ++step;
111 
112             *pos = TileCoordsXYZ(peep->GetLocation());
113 
114             EXPECT_PRED_FORMAT1(AssertIsNotForbiddenPosition, *pos);
115 
116             // Check that the peep is still on a footpath. Use next_z instead of pos->z here because pos->z will change
117             // when the peep is halfway up a slope, but next_z will not change until they move to the next tile.
118             EXPECT_NE(map_get_footpath_element({ pos->ToCoordsXY(), peep->NextLoc.z }), nullptr);
119         }
120 
121         // Clean up the peep, because we're reusing this loaded context for all tests.
122         peep_sprite_remove(peep);
123 
124         // Require that the number of steps taken is exactly what we expected. The pathfinder is supposed to be
125         // deterministic, and we reset the RNG seed for each test, everything should be entirely repeatable; as
126         // such a change in the number of steps taken on one of these paths needs to be reviewed. For the negative
127         // tests, we will not have reached the goal but we still expect the loop to have run for the total number
128         // of steps requested before giving up.
129         EXPECT_EQ(step, expectedSteps);
130 
131         return *pos == goal;
132     }
133 
AssertIsStartPosition(const char *,const TileCoordsXYZ & location)134     static ::testing::AssertionResult AssertIsStartPosition(const char*, const TileCoordsXYZ& location)
135     {
136         const uint32_t expectedSurfaceStyle = 11u;
137         const uint32_t style = map_get_surface_element_at(location.ToCoordsXYZ())->GetSurfaceStyle();
138 
139         if (style != expectedSurfaceStyle)
140             return ::testing::AssertionFailure()
141                 << "Start location " << location << " should have surface style " << expectedSurfaceStyle
142                 << " but actually has style " << style
143                 << ". Either the test map is not set up correctly, or you got the coordinates wrong.";
144 
145         return ::testing::AssertionSuccess();
146     }
147 
AssertIsNotForbiddenPosition(const char *,const TileCoordsXYZ & location)148     static ::testing::AssertionResult AssertIsNotForbiddenPosition(const char*, const TileCoordsXYZ& location)
149     {
150         const uint32_t forbiddenSurfaceStyle = 8u;
151 
152         const uint32_t style = map_get_surface_element_at(location.ToCoordsXYZ())->GetSurfaceStyle();
153 
154         if (style == forbiddenSurfaceStyle)
155             return ::testing::AssertionFailure()
156                 << "Path traversed location " << location << ", but it is marked as a forbidden location (surface style "
157                 << forbiddenSurfaceStyle << "). Either the map is set up incorrectly, or the pathfinder went the wrong way.";
158 
159         return ::testing::AssertionSuccess();
160     }
161 
162 private:
163     static std::shared_ptr<IContext> _context;
164 };
165 
166 std::shared_ptr<IContext> PathfindingTestBase::_context;
167 
168 struct SimplePathfindingScenario
169 {
170     const char* name;
171     TileCoordsXYZ start;
172     uint32_t steps;
173 
SimplePathfindingScenarioSimplePathfindingScenario174     SimplePathfindingScenario(const char* _name, const TileCoordsXYZ& _start, int _steps)
175         : name(_name)
176         , start(_start)
177         , steps(_steps)
178     {
179     }
180 
ToNameSimplePathfindingScenario181     static std::string ToName(const ::testing::TestParamInfo<SimplePathfindingScenario>& param_info)
182     {
183         return param_info.param.name;
184     }
185 };
186 
187 class SimplePathfindingTest : public PathfindingTestBase, public ::testing::WithParamInterface<SimplePathfindingScenario>
188 {
189 };
190 
TEST_P(SimplePathfindingTest,CanFindPathFromStartToGoal)191 TEST_P(SimplePathfindingTest, CanFindPathFromStartToGoal)
192 {
193     const SimplePathfindingScenario& scenario = GetParam();
194 
195     ASSERT_PRED_FORMAT1(AssertIsStartPosition, scenario.start);
196     TileCoordsXYZ pos = scenario.start;
197 
198     auto ride = FindRideByName(scenario.name);
199     ASSERT_NE(ride, nullptr);
200 
201     auto entrancePos = ride_get_entrance_location(ride, 0);
202     TileCoordsXYZ goal = TileCoordsXYZ(
203         entrancePos.x - TileDirectionDelta[entrancePos.direction].x,
204         entrancePos.y - TileDirectionDelta[entrancePos.direction].y, entrancePos.z);
205 
206     const auto succeeded = FindPath(&pos, goal, scenario.steps, ride->id) ? ::testing::AssertionSuccess()
207                                                                           : ::testing::AssertionFailure()
208             << "Failed to find path from " << scenario.start << " to " << goal << " in " << scenario.steps << " steps; reached "
209             << pos << " before giving up.";
210 
211     EXPECT_TRUE(succeeded);
212 }
213 
214 INSTANTIATE_TEST_CASE_P(
215     ForScenario, SimplePathfindingTest,
216     ::testing::Values(
217         SimplePathfindingScenario("StraightFlat", { 19, 15, 14 }, 24), SimplePathfindingScenario("SBend", { 15, 12, 14 }, 87),
218         SimplePathfindingScenario("UBend", { 17, 9, 14 }, 87), SimplePathfindingScenario("CBend", { 14, 5, 14 }, 164),
219         SimplePathfindingScenario("TwoEqualRoutes", { 9, 13, 14 }, 89),
220         SimplePathfindingScenario("TwoUnequalRoutes", { 3, 13, 14 }, 89),
221         SimplePathfindingScenario("StraightUpBridge", { 12, 15, 14 }, 24),
222         SimplePathfindingScenario("StraightUpSlope", { 14, 15, 14 }, 24),
223         SimplePathfindingScenario("SelfCrossingPath", { 6, 5, 14 }, 211)),
224     SimplePathfindingScenario::ToName);
225 
226 class ImpossiblePathfindingTest : public PathfindingTestBase, public ::testing::WithParamInterface<SimplePathfindingScenario>
227 {
228 };
229 
TEST_P(ImpossiblePathfindingTest,CannotFindPathFromStartToGoal)230 TEST_P(ImpossiblePathfindingTest, CannotFindPathFromStartToGoal)
231 {
232     const SimplePathfindingScenario& scenario = GetParam();
233     TileCoordsXYZ pos = scenario.start;
234     ASSERT_PRED_FORMAT1(AssertIsStartPosition, scenario.start);
235 
236     auto ride = FindRideByName(scenario.name);
237     ASSERT_NE(ride, nullptr);
238 
239     auto entrancePos = ride_get_entrance_location(ride, 0);
240     TileCoordsXYZ goal = TileCoordsXYZ(
241         entrancePos.x + TileDirectionDelta[entrancePos.direction].x,
242         entrancePos.y + TileDirectionDelta[entrancePos.direction].y, entrancePos.z);
243 
244     EXPECT_FALSE(FindPath(&pos, goal, 10000, ride->id));
245 }
246 
247 INSTANTIATE_TEST_CASE_P(
248     ForScenario, ImpossiblePathfindingTest,
249     ::testing::Values(
250         SimplePathfindingScenario("PathWithGap", { 1, 6, 14 }, 10000),
251         SimplePathfindingScenario("PathWithFences", { 11, 6, 14 }, 10000),
252         SimplePathfindingScenario("PathWithCliff", { 7, 17, 14 }, 10000)),
253     SimplePathfindingScenario::ToName);
254