mirror of
https://github.com/OpenRCT2/OpenRCT2
synced 2026-01-17 03:53:07 +01:00
Merge pull request #8539 from richard-fine/tests/pathfinding
Tests for peep pathfinding
This commit is contained in:
@@ -116,6 +116,16 @@ struct TileCoordsXYZ
|
||||
y += rhs.y;
|
||||
return *this;
|
||||
}
|
||||
|
||||
bool operator==(const TileCoordsXYZ& other) const
|
||||
{
|
||||
return x == other.x && y == other.y && z == other.z;
|
||||
}
|
||||
bool operator!=(const TileCoordsXYZ& other) const
|
||||
{
|
||||
return !(*this == other);
|
||||
}
|
||||
|
||||
int32_t x = 0, y = 0, z = 0;
|
||||
};
|
||||
|
||||
|
||||
@@ -197,3 +197,10 @@ if (NOT DISABLE_NETWORK)
|
||||
target_link_libraries(test_replays ${GTEST_LIBRARIES} libopenrct2 ${LDL} z)
|
||||
add_test(NAME replay_tests COMMAND test_replays)
|
||||
endif ()
|
||||
|
||||
# Pathfinding test
|
||||
set(PATHFINDING_TEST_SOURCES "${CMAKE_CURRENT_LIST_DIR}/Pathfinding.cpp"
|
||||
"${CMAKE_CURRENT_LIST_DIR}/TestData.cpp")
|
||||
add_executable(test_pathfinding ${PATHFINDING_TEST_SOURCES})
|
||||
target_link_libraries(test_pathfinding ${GTEST_LIBRARIES} libopenrct2 ${LDL} z)
|
||||
add_test(NAME pathfinding COMMAND test_pathfinding)
|
||||
|
||||
259
test/tests/Pathfinding.cpp
Normal file
259
test/tests/Pathfinding.cpp
Normal file
@@ -0,0 +1,259 @@
|
||||
#include "TestData.h"
|
||||
#include "openrct2/core/StringReader.hpp"
|
||||
#include "openrct2/peep/Peep.h"
|
||||
#include "openrct2/ride/Station.h"
|
||||
#include "openrct2/scenario/Scenario.h"
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
#include <openrct2/Context.h>
|
||||
#include <openrct2/Game.h>
|
||||
#include <openrct2/OpenRCT2.h>
|
||||
#include <openrct2/ParkImporter.h>
|
||||
#include <openrct2/platform/platform.h>
|
||||
#include <openrct2/world/Footpath.h>
|
||||
#include <openrct2/world/Map.h>
|
||||
|
||||
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
|
||||
gScenarioSrand0 = 0x12345678;
|
||||
gScenarioSrand1 = 0x87654321;
|
||||
}
|
||||
|
||||
static void TearDownTestCase()
|
||||
{
|
||||
_context = nullptr;
|
||||
}
|
||||
|
||||
protected:
|
||||
static Ride* FindRideByName(const char* name, int32_t* outRideIndex)
|
||||
{
|
||||
Ride* ride;
|
||||
FOR_ALL_RIDES ((*outRideIndex), ride)
|
||||
{
|
||||
char thisName[256];
|
||||
format_string(thisName, sizeof(thisName), ride->name, &ride->name_arguments);
|
||||
if (!_strnicmp(thisName, name, sizeof(thisName)))
|
||||
return ride;
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
static bool FindPath(TileCoordsXYZ* pos, const TileCoordsXYZ& goal, int expectedSteps, int 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 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;
|
||||
|
||||
// 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->guest_heading_to_ride_id = 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 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 < expectedSteps)
|
||||
{
|
||||
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);
|
||||
|
||||
// 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->x, pos->y, peep->next_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.x, location.y)->AsSurface()->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.x, location.y)->AsSurface()->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<IContext> _context;
|
||||
};
|
||||
|
||||
std::shared_ptr<IContext> 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<SimplePathfindingScenario>& param_info)
|
||||
{
|
||||
return param_info.param.name;
|
||||
}
|
||||
};
|
||||
|
||||
class SimplePathfindingTest : public PathfindingTestBase, public ::testing::WithParamInterface<SimplePathfindingScenario>
|
||||
{
|
||||
};
|
||||
|
||||
TEST_P(SimplePathfindingTest, CanFindPathFromStartToGoal)
|
||||
{
|
||||
const SimplePathfindingScenario& scenario = GetParam();
|
||||
|
||||
ASSERT_PRED_FORMAT1(AssertIsStartPosition, scenario.start);
|
||||
TileCoordsXYZ pos = scenario.start;
|
||||
|
||||
int32_t rideIndex;
|
||||
Ride* ride = FindRideByName(scenario.name, &rideIndex);
|
||||
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, rideIndex) ? ::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 }, 88),
|
||||
SimplePathfindingScenario("UBend", { 17, 9, 14 }, 86), SimplePathfindingScenario("CBend", { 14, 5, 14 }, 164),
|
||||
SimplePathfindingScenario("TwoEqualRoutes", { 9, 13, 14 }, 87),
|
||||
SimplePathfindingScenario("TwoUnequalRoutes", { 3, 13, 14 }, 87),
|
||||
SimplePathfindingScenario("StraightUpBridge", { 12, 15, 14 }, 24),
|
||||
SimplePathfindingScenario("StraightUpSlope", { 14, 15, 14 }, 24),
|
||||
SimplePathfindingScenario("SelfCrossingPath", { 6, 5, 14 }, 213)),
|
||||
SimplePathfindingScenario::ToName);
|
||||
|
||||
class ImpossiblePathfindingTest : public PathfindingTestBase, public ::testing::WithParamInterface<SimplePathfindingScenario>
|
||||
{
|
||||
};
|
||||
|
||||
TEST_P(ImpossiblePathfindingTest, CannotFindPathFromStartToGoal)
|
||||
{
|
||||
const SimplePathfindingScenario& scenario = GetParam();
|
||||
TileCoordsXYZ pos = scenario.start;
|
||||
ASSERT_PRED_FORMAT1(AssertIsStartPosition, scenario.start);
|
||||
|
||||
int32_t rideIndex;
|
||||
Ride* ride = FindRideByName(scenario.name, &rideIndex);
|
||||
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, rideIndex));
|
||||
}
|
||||
|
||||
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);
|
||||
BIN
test/tests/testdata/parks/pathfinding-tests.sv6
vendored
Normal file
BIN
test/tests/testdata/parks/pathfinding-tests.sv6
vendored
Normal file
Binary file not shown.
@@ -1,4 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project DefaultTargets="Build" ToolsVersion="14.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<PropertyGroup>
|
||||
<SolutionDir Condition="'$(SolutionDir)'==''">..\..\</SolutionDir>
|
||||
@@ -64,6 +64,7 @@
|
||||
<ClCompile Include="Localisation.cpp" />
|
||||
<ClCompile Include="MultiLaunch.cpp" />
|
||||
<ClCompile Include="ReplayTests.cpp" />
|
||||
<ClCompile Include="Pathfinding.cpp" />
|
||||
<ClCompile Include="RideRatings.cpp" />
|
||||
<ClCompile Include="sawyercoding_test.cpp" />
|
||||
<ClCompile Include="$(GtestDir)\src\gtest-all.cc" />
|
||||
|
||||
Reference in New Issue
Block a user