From 0dada5a75034bc243d666ba38615a0dbdaf41d40 Mon Sep 17 00:00:00 2001 From: frosch Date: Fri, 18 Apr 2025 22:48:18 +0200 Subject: [PATCH] Codechange: Add SpiralTileSequence to iterate over a tile area the same ways as CircularTileSearch. --- src/map.cpp | 1 + src/tests/CMakeLists.txt | 1 + src/tests/tilearea.cpp | 132 +++++++++++++++++++++++++++++++++++++++ src/tilearea.cpp | 102 ++++++++++++++++++++++++++++++ src/tilearea_type.h | 118 ++++++++++++++++++++++++++++++++++ 5 files changed, 354 insertions(+) create mode 100644 src/tests/tilearea.cpp diff --git a/src/map.cpp b/src/map.cpp index 315b89cba9..696d024be7 100644 --- a/src/map.cpp +++ b/src/map.cpp @@ -249,6 +249,7 @@ bool CircularTileSearch(TileIndex *tile, uint size, TestTileOnSearchProc proc, v /* If the length of the side is uneven, the center has to be checked * separately, as the pattern of uneven sides requires to go around the center */ if (proc(*tile, user_data)) return true; + if (size < 2) return false; /* If tile test is not successful, get one tile up, * ready for a test in first circle around center tile */ diff --git a/src/tests/CMakeLists.txt b/src/tests/CMakeLists.txt index 94579147fa..b346cd98c9 100644 --- a/src/tests/CMakeLists.txt +++ b/src/tests/CMakeLists.txt @@ -15,5 +15,6 @@ add_test_files( test_network_crypto.cpp test_script_admin.cpp test_window_desc.cpp + tilearea.cpp utf8.cpp ) diff --git a/src/tests/tilearea.cpp b/src/tests/tilearea.cpp new file mode 100644 index 0000000000..85c7b50acb --- /dev/null +++ b/src/tests/tilearea.cpp @@ -0,0 +1,132 @@ +/* + * This file is part of OpenTTD. + * OpenTTD is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, version 2. + * OpenTTD is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with OpenTTD. If not, see . + */ + +/** @file tilearea.cpp Test functionality from tilearea_type. */ + +#include "../stdafx.h" + +#include "../3rdparty/catch2/catch.hpp" + +#include "../tilearea_type.h" +#include "../map_func.h" + +#include "../safeguards.h" + +struct TileCoord { + uint x, y; +}; + +static void TestSpiralTileSequence(TileCoord center, uint diameter, std::span expected) +{ + auto tile = TileXY(center.x, center.y); + + std::vector result; + for (auto ti : SpiralTileSequence(tile, diameter)) { + result.push_back(ti); + } + REQUIRE(result.size() == expected.size()); + for (size_t i = 0; i < result.size(); ++i) { + CHECK(TileX(result[i]) == expected[i].x); + CHECK(TileY(result[i]) == expected[i].y); + } + + result.clear(); + CircularTileSearch(&tile, diameter, + +[](TileIndex tile, void *user_data) -> bool { + reinterpret_cast *>(user_data)->push_back(tile); + return false; + }, &result); + REQUIRE(result.size() == expected.size()); + for (size_t i = 0; i < result.size(); ++i) { + CHECK(TileX(result[i]) == expected[i].x); + CHECK(TileY(result[i]) == expected[i].y); + } +} + +static void TestSpiralTileSequence(TileCoord start_north, uint radius, uint w, uint h, std::span expected) +{ + auto tile = TileXY(start_north.x, start_north.y); + + std::vector result; + for (auto ti : SpiralTileSequence(tile, radius, w, h)) { + result.push_back(ti); + } + REQUIRE(result.size() == expected.size()); + for (size_t i = 0; i < result.size(); ++i) { + CHECK(TileX(result[i]) == expected[i].x); + CHECK(TileY(result[i]) == expected[i].y); + } + + result.clear(); + CircularTileSearch(&tile, radius, w, h, + +[](TileIndex tile, void *user_data) -> bool { + reinterpret_cast *>(user_data)->push_back(tile); + return false; + }, &result); + REQUIRE(result.size() == expected.size()); + for (size_t i = 0; i < result.size(); ++i) { + CHECK(TileX(result[i]) == expected[i].x); + CHECK(TileY(result[i]) == expected[i].y); + } +} + +TEST_CASE("SpiralTileSequence - minimum") +{ + Map::Allocate(64, 64); + + TileCoord expected[] = {{63, 63}}; + TestSpiralTileSequence({63, 63}, 1, expected); + TestSpiralTileSequence({63, 63}, 2, expected); + TestSpiralTileSequence({63, 63}, 1, 0, 0, expected); + TestSpiralTileSequence({63, 63}, 1, 2, 2, expected); +} + +TEST_CASE("SpiralTileSequence - odd") +{ + Map::Allocate(64, 64); + + TileCoord expected[] = { + {1, 1}, + {2, 0}, {1, 0}, {0, 0}, {0, 1}, {0, 2}, {1, 2}, {2, 2}, {2, 1}, + {0, 3}, {1, 3}, {2, 3}, {3, 3}, {3, 2}, {3, 1}, {3, 0}, + }; + TestSpiralTileSequence({1, 1}, 5, expected); +} + +TEST_CASE("SpiralTileSequence - even") +{ + Map::Allocate(64, 64); + + TileCoord expected[] = { + {2, 1}, {1, 1}, {1, 2}, {2, 2}, + {3, 0}, {2, 0}, {1, 0}, {0, 0}, {0, 1}, {0, 2}, {0, 3}, {1, 3}, {2, 3}, {3, 3}, {3, 2}, {3, 1}, + {0, 4}, {1, 4}, {2, 4}, {3, 4}, {4, 4}, {4, 3}, {4, 2}, {4, 1}, {4, 0}, + }; + TestSpiralTileSequence({1, 1}, 6, expected); + TestSpiralTileSequence({1, 1}, 3, 0, 0, expected); +} + +TEST_CASE("SpiralTileSequence - zero hole") +{ + Map::Allocate(64, 64); + + TileCoord expected[] = { + {5, 2}, {4, 2}, {3, 2}, {2, 2}, {2, 3}, {3, 3}, {4, 3}, {5, 3}, + {6, 1}, {5, 1}, {4, 1}, {3, 1}, {2, 1}, {1, 1}, {1, 2}, {1, 3}, {1, 4}, {2, 4}, {3, 4}, {4, 4}, {5, 4}, {6, 4}, {6, 3}, {6, 2}, + }; + TestSpiralTileSequence({2, 2}, 2, 2, 0, expected); +} + +TEST_CASE("SpiralTileSequence - normal hole") +{ + Map::Allocate(64, 64); + + TileCoord expected[] = { + {4, 2}, {3, 2}, {2, 2}, {2, 3}, {2, 4}, {2, 5}, {3, 5}, {4, 5}, {4, 4}, {4, 3}, + }; + TestSpiralTileSequence({2, 2}, 1, 1, 2, expected); +} diff --git a/src/tilearea.cpp b/src/tilearea.cpp index 83705a82c1..09560c9f5c 100644 --- a/src/tilearea.cpp +++ b/src/tilearea.cpp @@ -295,3 +295,105 @@ TileIterator &DiagonalTileIterator::operator++() } return std::make_unique(corner1, corner2); } + +/** + * See SpiralTileSequence constructor for description. + */ +SpiralTileIterator::SpiralTileIterator(TileIndex center, uint diameter) : + max_radius(diameter / 2), + cur_radius(0), + dir(DIAGDIR_BEGIN) +{ + assert(diameter > 0); + + if (diameter % 2 == 1) { + this->extent.fill(1); + this->dir = INVALID_DIAGDIR; // special case for odd diameters, see Increment() + this->position = 0; + + this->x = TileX(center); + this->y = TileY(center); + } else { + this->extent.fill(0); + this->dir = DIAGDIR_BEGIN; + this->InitPosition(); + + /* Start with the west corner of the center 2x2 rect */ + this->x = TileX(center) + 1; + this->y = TileY(center); + } + this->SkipOutsideMap(); +} + +/** + * See SpiralTileSequence constructor for description. + */ +SpiralTileIterator::SpiralTileIterator(TileIndex start_north, uint radius, uint w, uint h) : + max_radius(radius), + extent{w, h, w, h}, + cur_radius(0), + dir(DIAGDIR_BEGIN), + /* first tile is the west corner */ + x(TileX(start_north) + w + 1), + y(TileY(start_north)) +{ + assert(max_radius > 0); + this->InitPosition(); + this->SkipOutsideMap(); +} + +/** + * Advance the internal state until it reaches a valid tile or the end. + */ +void SpiralTileIterator::SkipOutsideMap() +{ + while (!this->IsEnd() && (this->x >= Map::SizeX() || this->y >= Map::SizeY())) this->Increment(); +} + +/** + * Initialise "position" after "dir" was changed. + */ +void SpiralTileIterator::InitPosition() +{ + this->position = this->extent[this->dir] + this->cur_radius * 2 + 1; +} + +/** + * Advance the internal state to the next potential tile. + * The tile may be outside the map though. + */ +void SpiralTileIterator::Increment() +{ + assert(!this->IsEnd()); + + /* Special value for first tile in areas with odd diameter */ + if (this->dir == INVALID_DIAGDIR) { + const auto west = TileIndexDiffCByDir(DIR_W); + this->x += west.x; + this->y += west.y; + this->dir = DIAGDIR_BEGIN; + this->InitPosition(); + return; + } + + /* Step to the next 'neighbour' in the circular line */ + const auto diff = TileIndexDiffCByDiagDir(this->dir); + this->x += diff.x; + this->y += diff.y; + --this->position; + if (this->position > 0) return; + + /* Corner reached, switch direction */ + ++this->dir; + + if (this->dir == DIAGDIR_END) { + /* Jump to next circle */ + const auto west = TileIndexDiffCByDir(DIR_W); + this->x += west.x; + this->y += west.y; + ++this->cur_radius; + this->dir = DIAGDIR_BEGIN; + } + + this->InitPosition(); +} diff --git a/src/tilearea_type.h b/src/tilearea_type.h index 9add7cfe8b..85a910a97b 100644 --- a/src/tilearea_type.h +++ b/src/tilearea_type.h @@ -253,4 +253,122 @@ public: } }; +/** + * Helper class for SpiralTileSequence. + */ +class SpiralTileIterator { +public: + using value_type = TileIndex; + using difference_type = std::ptrdiff_t; + using iterator_category = std::forward_iterator_tag; + using pointer = void; + using reference = void; + + SpiralTileIterator(TileIndex center, uint diameter); + SpiralTileIterator(TileIndex start_north, uint radius, uint w, uint h); + + bool operator==(const SpiralTileIterator &rhs) const { return this->x == rhs.x && this->y == rhs.y; } + bool operator==(const std::default_sentinel_t &) const { return this->IsEnd(); } + + TileIndex operator*() const { return TileXY(this->x, this->y); } + + SpiralTileIterator &operator++() + { + this->Increment(); + this->SkipOutsideMap(); + return *this; + } + + SpiralTileIterator operator++(int) + { + SpiralTileIterator result = *this; + ++*this; + return result; + } + +private: + /* set by constructor, const afterwards */ + uint max_radius; + std::array extent; + + /* mutable iterator state */ + uint cur_radius; + DiagDirection dir; + uint position; + uint x, y; + + void SkipOutsideMap(); + void InitPosition(); + void Increment(); + + /** + * Test whether the iterator reached the end. + */ + bool IsEnd() const + { + return this->cur_radius == this->max_radius && this->dir != INVALID_DIAGDIR; + } +}; + +/** + * Generate TileIndices around a center tile or tile area, with increasing distance. + */ +class SpiralTileSequence { +public: + /** + * Generate TileIndices for a square area around a center tile. + * + * The size of the square is given by the length of the edge. + * If the size is even, the south extent will be larger than the north extent. + * + * Example for diameter=4, [ ] is the "center": + * 1 + * 1 1 + * 1 [0] 1 + * 1 0 0 1 + * 1 0 1 + * 1 1 + * 1 + * The sequence starts with the "0" tiles, and continues with the shells around it. + * + * @param center Center of the square area. + * @param diameter Edge length of the square. + * @pre diameter > 0 + * @note This constructor uses a "diameter", unlike the other constructor using a "radius". + */ + SpiralTileSequence(TileIndex center, uint diameter) : start(center, diameter) {} + + /** + * Generate TileIndices for a rectangular area with an optional rectangular hole in the center. + * The TileIndices will be sorted by increasing distance from the center (hole). + * + * Example for radius=2, w=2, h=1, [ ] is "start_north": + * 1 + * 1 1 + * 1 [0] 1 + * 1 0 0 1 + * 1 0 H 0 1 + * 1 0 H 0 1 + * 1 0 0 1 + * 1 0 1 + * 1 1 + * 1 + * The sequence starts with the "0" tiles, and continues with the shells around it. + * + * @param start_north Tile directly north from the center hole. + * @param radius Radial distance between outer rectangle and center hole. + * @param w Width of the inner rectangular hole. + * @param h Height of the inner rectangular hole. + * @pre radius > 0 + * @note This constructor uses a "radius", unlike the other constructor using a "diameter". + */ + SpiralTileSequence(TileIndex start_north, uint radius, uint w, uint h) : start(start_north, radius, w, h) {} + + SpiralTileIterator begin() const { return start; } + std::default_sentinel_t end() const { return std::default_sentinel_t(); } + +private: + SpiralTileIterator start; +}; + #endif /* TILEAREA_TYPE_H */