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 }