1 /**
2  * Provides various useful operations on a tile grid.
3  */
4 module dtiled.algorithm;
5 
6 import std.range;
7 import std.typecons : Tuple;
8 import std.algorithm;
9 import std.container : Array, SList;
10 import dtiled.coords : RowCol, Diagonals;
11 import dtiled.grid;
12 
13 /// Same as enclosedTiles, but return coords instead of tiles
14 auto enclosedCoords(alias isWall, T)(T grid, RowCol origin, Diagonals diags = Diagonals.no)
15   if (is(typeof(isWall(grid.tileAt(RowCol(0,0)))) : bool))
16 {
17   // track whether we have hit the edge of the map
18   bool hitEdge;
19 
20   // keep a flag for each tile to mark which have been visited
21   Array!bool visited;
22   visited.length = grid.numRows * grid.numCols;
23 
24   // visited[] index for a (row,col) pair
25   auto coordToIdx(RowCol coord) {
26     return coord.row * grid.numCols + coord.col;
27   }
28 
29   // row/col coord for a visited[] index
30   auto idxToCoord(size_t idx) {
31     return RowCol(idx / grid.numCols, idx % grid.numCols);
32   }
33 
34   bool outOfBounds(RowCol coord) {
35     return coord.row < 0 || coord.col < 0 || coord.row >= grid.numRows || coord.col >= grid.numCols;
36   }
37 
38   void flood(RowCol coord) {
39     auto idx = coordToIdx(coord);
40     hitEdge = hitEdge || outOfBounds(coord);
41 
42     // break this recursive branch if we hit an edge or a visited or invalid tile.
43     if (hitEdge || visited[idx] || isWall(grid.tileAt(coord))) return;
44 
45     visited[idx] = true;
46 
47     // recurse into neighboring tiles
48     foreach(neighbor ; coord.adjacent(diags)) flood(neighbor);
49   }
50 
51   // start the flood at the origin tile
52   flood(origin);
53 
54   return visited[]
55     .enumerate                            // pair each bool with an index
56     .filter!(pair => pair.value)          // keep only the visited nodes
57     .map!(pair => idxToCoord(pair.index)) // grab the tile for each visited node
58     .take(hitEdge ? 0 : size_t.max);      // empty range if edge of map was touched
59 }
60 
61 /**
62  * Find an area of tiles enclosed by 'walls'.
63  *
64  * Params:
65  *  isWall = predicate which returns true if a tile should be considered a 'wall'
66  *  grid = grid of tiles to find enclosed area in
67  *  origin = tile that may be part of an enclosed region
68  *  diags = if yes, an area is not considered enclosed if there is a diagonal opening.
69  *
70  * Returns: a range of tiles in the enclosure (empty if origin is not part of an enclosed region)
71  */
72 auto enclosedTiles(alias isWall, T)(T grid, RowCol origin, Diagonals diags = Diagonals.no)
73   if (is(typeof(isWall(grid.tileAt(RowCol(0,0)))) : bool))
74 {
75   return enclosedCoords!isWall(grid, origin, diags).map!(x => grid.tileAt(x));
76 }
77 
78 ///
79 unittest {
80   import std.array;
81   import std.algorithm : equal;
82 
83   // let the 'X's represent 'walls', and the other letters 'open' areas we'd link to identify
84   auto tiles = rectGrid([
85     // 0    1    2    3    4    5 <-col| row
86     [ 'X', 'X', 'X', 'X', 'X', 'X' ], // 0
87     [ 'X', 'a', 'a', 'X', 'b', 'X' ], // 1
88     [ 'X', 'a', 'a', 'X', 'b', 'X' ], // 2
89     [ 'X', 'X', 'X', 'X', 'X', 'X' ], // 3
90     [ 'd', 'd', 'd', 'X', 'c', 'X' ], // 4
91     [ 'd', 'd', 'd', 'X', 'X', 'c' ], // 5
92   ]);
93 
94   static bool isWall(char c) { return c == 'X'; }
95 
96   // starting on a wall should return an empty result
97   assert(tiles.enclosedTiles!isWall(RowCol(0, 0)).empty);
98 
99   // all tiles in the [1,1] -> [2,2] area should find the 'a' room
100   assert(tiles.enclosedTiles!isWall(RowCol(1, 1)).equal(['a', 'a', 'a', 'a']));
101   assert(tiles.enclosedTiles!isWall(RowCol(1, 2)).equal(['a', 'a', 'a', 'a']));
102   assert(tiles.enclosedTiles!isWall(RowCol(2, 1)).equal(['a', 'a', 'a', 'a']));
103   assert(tiles.enclosedTiles!isWall(RowCol(2, 2)).equal(['a', 'a', 'a', 'a']));
104 
105   // get the two-tile 'b' room at [1,4] -> [2,4]
106   assert(tiles.enclosedTiles!isWall(RowCol(1, 4)).equal(['b', 'b']));
107   assert(tiles.enclosedTiles!isWall(RowCol(2, 4)).equal(['b', 'b']));
108 
109   // get the single tile 'c' room at 4,4
110   assert(tiles.enclosedTiles!isWall(RowCol(4, 4)).equal(['c']));
111   // if we require that diagonals be blocked too, 'c' is not an enclosure
112   assert(tiles.enclosedTiles!isWall(RowCol(4, 4), Diagonals.yes).empty);
113 
114   // the 'd' region is not an enclosure (touches map edge)
115   assert(tiles.enclosedTiles!isWall(RowCol(4, 1)).empty);
116   assert(tiles.enclosedTiles!isWall(RowCol(5, 0)).empty);
117 }
118 
119 /// Same as floodTiles, but return coordinates instead of the tiles at those coordinates.
120 auto floodCoords(alias pred, T)(T grid, RowCol origin, Diagonals diags = Diagonals.no)
121   if (is(typeof(pred(grid.tileAt(RowCol(0,0)))) : bool))
122 {
123   struct Result {
124     private {
125       T            _grid;
126       SList!RowCol _stack;
127       Array!bool   _visited;
128 
129       // helpers to translate between the 2D grid coordinate space and the 1D visited array
130       bool getVisited(RowCol coord) {
131         auto idx = coord.row * grid.numCols + coord.col;
132         return _visited[idx];
133       }
134 
135       void setVisited(RowCol coord) {
136         auto idx = coord.row * grid.numCols + coord.col;
137         _visited[idx] = true;
138       }
139 
140       // true if front is out of bounds, already visited, or does not meet the predicate
141       bool shouldSkipFront() {
142         return !_grid.contains(front) || getVisited(front) || !pred(_grid.tileAt(front));
143       }
144     }
145 
146     this(T grid, RowCol origin) {
147       _grid = grid;
148       _visited.length = grid.numRows * grid.numCols; // one visited entry for each tile
149 
150       // push the first tile onto the stack only if it meets the predicate
151       if (pred(grid.tileAt(origin))) {
152         _stack.insertFront(origin);
153       }
154     }
155 
156     @property auto front() { return _stack.front; }
157     @property bool empty() { return _stack.empty; }
158 
159     void popFront() {
160       // copy the current coord before we pop it
161       auto coord = front;
162 
163       // mark that the current coord was visited and pop it
164       setVisited(coord);
165       _stack.removeFront();
166 
167       // push neighboring coords onto the stack
168       foreach(neighbor ; coord.adjacent(diags)) { _stack.insert(neighbor); }
169 
170       // keep popping until stack is empty or we get a floodable coord
171       while (!_stack.empty && shouldSkipFront()) { _stack.removeFront(); }
172     }
173   }
174 
175   return Result(grid, origin);
176 }
177 
178 /**
179  * Returns a range that iterates through tiles based on a flood filling algorithm.
180  *
181  * Params:
182  *  pred   = predicate that returns true if the flood should progress through a given tile.
183  *  grid   = grid to apply flood to.
184  *  origin = coordinate at which to begin flood.
185  *  diags  = by default, flood only progresses to directly adjacent tiles.
186  *           Diagonals.yes causes the flood to progress across diagonals too.
187  */
188 auto floodTiles(alias pred, T)(T grid, RowCol origin, Diagonals diags = Diagonals.no)
189   if (is(typeof(pred(grid.tileAt(RowCol(0,0)))) : bool))
190 {
191   return floodCoords!pred(grid, origin, diags).map!(x => grid.tileAt(x));
192 }
193 
194 ///
195 unittest {
196   import std.array;
197   import std.algorithm : equal;
198 
199   // let the 'X's represent 'walls', and the other letters 'open' areas we'd link to identify
200   auto grid = rectGrid([
201     // 0    1    2    3    4    5 <-col| row
202     [ 'X', 'X', 'X', 'X', 'X', 'X' ], // 0
203     [ 'X', 'a', 'a', 'X', 'b', 'X' ], // 1
204     [ 'X', 'a', 'a', 'X', 'b', 'X' ], // 2
205     [ 'X', 'X', 'X', 'X', 'X', 'c' ], // 3
206     [ 'd', 'd', 'd', 'X', 'c', 'X' ], // 4
207     [ 'd', 'd', 'd', 'X', 'X', 'X' ], // 5
208   ]);
209 
210   // starting on a wall should return an empty result
211   assert(grid.floodTiles!(x => x == 'a')(RowCol(0,0)).empty);
212   assert(grid.floodTiles!(x => x == 'a')(RowCol(3,3)).empty);
213 
214   // flood the 'a' room
215   assert(grid.floodTiles!(x => x == 'a')(RowCol(1,1)).equal(['a', 'a', 'a', 'a']));
216   assert(grid.floodTiles!(x => x == 'a')(RowCol(1,2)).equal(['a', 'a', 'a', 'a']));
217   assert(grid.floodTiles!(x => x == 'a')(RowCol(2,1)).equal(['a', 'a', 'a', 'a']));
218   assert(grid.floodTiles!(x => x == 'a')(RowCol(2,2)).equal(['a', 'a', 'a', 'a']));
219 
220   // flood the 'a' room, but asking for a 'b'
221   assert(grid.floodTiles!(x => x == 'b')(RowCol(2,2)).empty);
222 
223   // flood the 'b' room
224   assert(grid.floodTiles!(x => x == 'b')(RowCol(1,4)).equal(['b', 'b']));
225 
226   // flood the 'c' room
227   assert(grid.floodTiles!(x => x == 'c')(RowCol(4,4)).equal(['c']));
228 
229   // flood the 'd' room
230   assert(grid.floodTiles!(x => x == 'd')(RowCol(4,1)).equal(['d', 'd', 'd', 'd', 'd', 'd']));
231 
232   // flood the 'b' and 'c' rooms, moving through diagonals
233   assert(grid.floodTiles!(x => x == 'b' || x == 'c')(RowCol(4,4), Diagonals.yes)
234       .equal(['c', 'c', 'b', 'b']));
235 }