#include "catch.hpp" #include #include #include #include #include #include "BuildingType.h" #include "Rotation.h" #include "TunnelCompletion.h" // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- namespace { using TunnelMap = std::map, TunnelTileInfo>; TunnelLookup makeLookup(const TunnelMap& tunnels) { return [&tunnels](QPoint tile) -> std::optional { const TunnelMap::const_iterator it = tunnels.find({tile.x(), tile.y()}); if (it == tunnels.end()) { return std::nullopt; } return it->second; }; } // Cursor at the centre of a tile. QVector2D tileCentre(QPoint tile) { return QVector2D(static_cast(tile.x()) + 0.5f, static_cast(tile.y()) + 0.5f); } } // namespace // --------------------------------------------------------------------------- // No match // --------------------------------------------------------------------------- TEST_CASE("No tunnels: ghost defaults to a Tunnel Entry with no partner") { const TunnelMap tunnels; const TunnelCompletion result = resolveTunnelCompletion( makeLookup(tunnels), QPoint(5, 5), Rotation::East, 10, tileCentre(QPoint(5, 5))); REQUIRE(result.resolvedType == BuildingType::TunnelEntry); REQUIRE(!result.partnerTile.has_value()); } // --------------------------------------------------------------------------- // Entry-completion: an existing exit ahead keeps the ghost an entry // --------------------------------------------------------------------------- TEST_CASE("Existing exit ahead completes as an entry") { TunnelMap tunnels; tunnels[{8, 5}] = TunnelTileInfo{BuildingType::TunnelExit, Rotation::East}; const TunnelCompletion result = resolveTunnelCompletion( makeLookup(tunnels), QPoint(5, 5), Rotation::East, 10, tileCentre(QPoint(5, 5))); REQUIRE(result.resolvedType == BuildingType::TunnelEntry); REQUIRE(result.partnerTile.has_value()); REQUIRE(*result.partnerTile == QPoint(8, 5)); } // --------------------------------------------------------------------------- // Exit-completion: an existing entry behind flips the ghost to an exit // --------------------------------------------------------------------------- TEST_CASE("Existing entry behind completes as an exit") { TunnelMap tunnels; tunnels[{2, 5}] = TunnelTileInfo{BuildingType::TunnelEntry, Rotation::East}; const TunnelCompletion result = resolveTunnelCompletion( makeLookup(tunnels), QPoint(5, 5), Rotation::East, 10, tileCentre(QPoint(5, 5))); REQUIRE(result.resolvedType == BuildingType::TunnelExit); REQUIRE(result.partnerTile.has_value()); REQUIRE(*result.partnerTile == QPoint(2, 5)); } // --------------------------------------------------------------------------- // Both matches: the sub-tile cursor position breaks the tie // --------------------------------------------------------------------------- TEST_CASE("Both matches resolve to the partner nearer the sub-tile cursor") { TunnelMap tunnels; tunnels[{3, 5}] = TunnelTileInfo{BuildingType::TunnelEntry, Rotation::East}; // behind tunnels[{7, 5}] = TunnelTileInfo{BuildingType::TunnelExit, Rotation::East}; // ahead const TunnelLookup lookup = makeLookup(tunnels); SECTION("cursor near the left edge completes the entry (ghost becomes exit)") { const QVector2D cursor(5.1f, 5.5f); const TunnelCompletion result = resolveTunnelCompletion(lookup, QPoint(5, 5), Rotation::East, 10, cursor); REQUIRE(result.resolvedType == BuildingType::TunnelExit); REQUIRE(*result.partnerTile == QPoint(3, 5)); } SECTION("cursor near the right edge completes the exit (ghost stays entry)") { const QVector2D cursor(5.9f, 5.5f); const TunnelCompletion result = resolveTunnelCompletion(lookup, QPoint(5, 5), Rotation::East, 10, cursor); REQUIRE(result.resolvedType == BuildingType::TunnelEntry); REQUIRE(*result.partnerTile == QPoint(7, 5)); } } // --------------------------------------------------------------------------- // A same-direction tunnel between the ghost and a candidate blocks the pairing // --------------------------------------------------------------------------- TEST_CASE("A same-direction entry between the ghost and an exit blocks completion") { TunnelMap tunnels; tunnels[{7, 5}] = TunnelTileInfo{BuildingType::TunnelEntry, Rotation::East}; // blocker tunnels[{9, 5}] = TunnelTileInfo{BuildingType::TunnelExit, Rotation::East}; const TunnelCompletion result = resolveTunnelCompletion( makeLookup(tunnels), QPoint(5, 5), Rotation::East, 10, tileCentre(QPoint(5, 5))); // The forward scan stops at the same-direction entry, which is not an exit, so no // entry-completion. Nothing behind, so no exit-completion either. REQUIRE(result.resolvedType == BuildingType::TunnelEntry); REQUIRE(!result.partnerTile.has_value()); } // --------------------------------------------------------------------------- // Differently-facing tunnels are skipped by the scan // --------------------------------------------------------------------------- TEST_CASE("A differently-facing tunnel ahead is skipped, not treated as a match") { TunnelMap tunnels; tunnels[{6, 5}] = TunnelTileInfo{BuildingType::TunnelExit, Rotation::North}; // skipped tunnels[{8, 5}] = TunnelTileInfo{BuildingType::TunnelExit, Rotation::East}; // match const TunnelCompletion result = resolveTunnelCompletion( makeLookup(tunnels), QPoint(5, 5), Rotation::East, 10, tileCentre(QPoint(5, 5))); REQUIRE(result.resolvedType == BuildingType::TunnelEntry); REQUIRE(*result.partnerTile == QPoint(8, 5)); } // --------------------------------------------------------------------------- // Distance limit // --------------------------------------------------------------------------- TEST_CASE("A candidate beyond the max distance is not matched") { TunnelMap tunnels; tunnels[{8, 5}] = TunnelTileInfo{BuildingType::TunnelExit, Rotation::East}; // distance 3 const TunnelCompletion result = resolveTunnelCompletion( makeLookup(tunnels), QPoint(5, 5), Rotation::East, 2, tileCentre(QPoint(5, 5))); REQUIRE(result.resolvedType == BuildingType::TunnelEntry); REQUIRE(!result.partnerTile.has_value()); } // --------------------------------------------------------------------------- // findTunnelPartner: the matching end of an existing selected tunnel // (REQ-BLD-TUNNEL-SELECT-HIGHLIGHT) // --------------------------------------------------------------------------- TEST_CASE("findTunnelPartner: a selected entry finds its exit ahead") { TunnelMap tunnels; tunnels[{2, 5}] = TunnelTileInfo{BuildingType::TunnelEntry, Rotation::East}; tunnels[{6, 5}] = TunnelTileInfo{BuildingType::TunnelExit, Rotation::East}; const std::optional partner = findTunnelPartner( makeLookup(tunnels), QPoint(2, 5), BuildingType::TunnelEntry, Rotation::East, 10); REQUIRE(partner.has_value()); REQUIRE(*partner == QPoint(6, 5)); } TEST_CASE("findTunnelPartner: a selected exit finds its entry behind") { TunnelMap tunnels; tunnels[{2, 5}] = TunnelTileInfo{BuildingType::TunnelEntry, Rotation::East}; tunnels[{6, 5}] = TunnelTileInfo{BuildingType::TunnelExit, Rotation::East}; const std::optional partner = findTunnelPartner( makeLookup(tunnels), QPoint(6, 5), BuildingType::TunnelExit, Rotation::East, 10); REQUIRE(partner.has_value()); REQUIRE(*partner == QPoint(2, 5)); } TEST_CASE("findTunnelPartner: an unpaired tunnel has no partner") { TunnelMap tunnels; tunnels[{2, 5}] = TunnelTileInfo{BuildingType::TunnelEntry, Rotation::East}; REQUIRE(!findTunnelPartner(makeLookup(tunnels), QPoint(2, 5), BuildingType::TunnelEntry, Rotation::East, 10).has_value()); } TEST_CASE("findTunnelPartner: a same-direction tunnel between blocks the pairing") { TunnelMap tunnels; tunnels[{2, 5}] = TunnelTileInfo{BuildingType::TunnelEntry, Rotation::East}; // selected tunnels[{4, 5}] = TunnelTileInfo{BuildingType::TunnelEntry, Rotation::East}; // blocker tunnels[{6, 5}] = TunnelTileInfo{BuildingType::TunnelExit, Rotation::East}; // The scan from the selected entry stops at the same-direction entry, which is not // an exit, so the exit beyond it is not paired. REQUIRE(!findTunnelPartner(makeLookup(tunnels), QPoint(2, 5), BuildingType::TunnelEntry, Rotation::East, 10).has_value()); } TEST_CASE("findTunnelPartner: a differently-facing tunnel is skipped") { TunnelMap tunnels; tunnels[{2, 5}] = TunnelTileInfo{BuildingType::TunnelEntry, Rotation::East}; tunnels[{4, 5}] = TunnelTileInfo{BuildingType::TunnelExit, Rotation::North}; // skipped tunnels[{6, 5}] = TunnelTileInfo{BuildingType::TunnelExit, Rotation::East}; // match const std::optional partner = findTunnelPartner( makeLookup(tunnels), QPoint(2, 5), BuildingType::TunnelEntry, Rotation::East, 10); REQUIRE(partner.has_value()); REQUIRE(*partner == QPoint(6, 5)); } TEST_CASE("findTunnelPartner: a partner beyond the max distance is not matched") { TunnelMap tunnels; tunnels[{2, 5}] = TunnelTileInfo{BuildingType::TunnelEntry, Rotation::East}; tunnels[{6, 5}] = TunnelTileInfo{BuildingType::TunnelExit, Rotation::East}; // distance 4 REQUIRE(!findTunnelPartner(makeLookup(tunnels), QPoint(2, 5), BuildingType::TunnelEntry, Rotation::East, 3).has_value()); }