1 /** 2 * A grid wraps a 2D array to provide specialized grid-based functionality. 3 * 4 * Currently, the only grid type is RectGrid, which can serve as the base for an orthogonal or isometric map. 5 * HexGrid may be added in a later version to support hexagonal maps. 6 * 7 * Authors: <a href="https://github.com/rcorre">rcorre</a> 8 * License: <a href="http://opensource.org/licenses/MIT">MIT</a> 9 * Copyright: Copyright © 2015, Ryan Roden-Corrent 10 */ 11 module dtiled.grid; 12 13 import std.range : only, chain, takeNone, hasLength; 14 import std.range : isInputRange, isForwardRange, isBidirectionalRange; 15 import std.format : format; 16 import std.algorithm : all, map, filter; 17 import std.exception : enforce; 18 import dtiled.coords; 19 20 /// True if T is a static or dynamic array type. 21 enum isArray2D(T) = is(typeof(T.init[0][0])) && // 2D random access 22 is(typeof(T.init.length) : size_t) && // stores count of rows 23 is(typeof(T.init[0].length) : size_t); // stores count of columns 24 25 /// 26 unittest { 27 import std.container : Array; 28 29 static assert(isArray2D!(int[][])); 30 static assert(isArray2D!(char[3][5])); 31 static assert(isArray2D!(Array!(Array!int))); 32 } 33 34 /// Convenience function to wrap a RectGrid around a 2D array. 35 auto rectGrid(T)(T tiles) if (isArray2D!T) { return RectGrid!T(tiles); } 36 37 /// 38 unittest { 39 auto dynamicArray = [ 40 [1,2,3], 41 [4,5,6] 42 ]; 43 auto dynamicGrid = rectGrid(dynamicArray); 44 assert(dynamicGrid.numRows == 2 && dynamicGrid.numCols == 3); 45 static assert(is(dynamicGrid.TileType == int)); 46 47 char[3][2] staticArray = [ 48 [ 'a', 'a', 'a' ], 49 [ 'a', 'a', 'a' ], 50 ]; 51 auto staticGrid = rectGrid(staticArray); 52 assert(staticGrid.numRows == 2 && staticGrid.numCols == 3); 53 static assert(is(staticGrid.TileType == char)); 54 } 55 56 /// A grid of rectangular tiles. Wraps a 2D array to provide grid-based access to tiles. 57 struct RectGrid(T) if (isArray2D!T) { 58 private T _tiles; 59 60 /// The type used to represent a tile in this grid 61 alias TileType = typeof(_tiles[0][0]); 62 63 /// Construct a grid from a 2D tile array. See rectGrid for a constructor with type inference. 64 this(T tiles) { 65 assertNotJagged(tiles, "RectGrid cannot be a constructed from a jagged array"); 66 _tiles = tiles; 67 } 68 69 /// Number of columns along a grid's x axis. 70 auto numCols() { return _tiles[0].length; } 71 72 /// 73 unittest { 74 auto grid = rectGrid([ 75 [ 0, 0, 0, 0 ], 76 [ 0, 0, 0, 0 ], 77 ]); 78 79 assert(grid.numCols == 4); 80 } 81 82 /// Number of rows along a grid's y axis. 83 auto numRows() { return _tiles.length; } 84 85 unittest { 86 auto grid = rectGrid([ 87 [ 0, 0, 0, 0 ], 88 [ 0, 0, 0, 0 ], 89 ]); 90 91 assert(grid.numRows == 2); 92 } 93 94 /// The total number of tiles in a grid. 95 auto numTiles() { return this.numRows * this.numCols; } 96 97 unittest { 98 auto grid = rectGrid([ 99 [ 0, 0, 0, 0 ], 100 [ 0, 0, 0, 0 ], 101 ]); 102 103 assert(grid.numTiles == 8); 104 } 105 106 /** 107 * True if the grid coordinate is within the grid bounds. 108 */ 109 bool contains(RowCol coord) { 110 return 111 coord.row >= 0 && 112 coord.col >= 0 && 113 coord.row < this.numRows && 114 coord.col < this.numCols; 115 } 116 117 /// 118 unittest { 119 // 5x3 map 120 auto grid = rectGrid([ 121 //0 1 2 3 4 col row 122 [ 0, 0, 0, 0, 0 ], // 0 123 [ 0, 0, 0, 0, 0 ], // 1 124 [ 0, 0, 0, 0, 0 ], // 2 125 ]); 126 127 assert( grid.contains(RowCol(0 , 0))); // top left 128 assert( grid.contains(RowCol(2 , 4))); // bottom right 129 assert( grid.contains(RowCol(1 , 2))); // center 130 assert(!grid.contains(RowCol(0 , 5))); // beyond right border 131 assert(!grid.contains(RowCol(3 , 0))); // beyond bottom border 132 assert(!grid.contains(RowCol(0 ,-1))); // beyond left border 133 assert(!grid.contains(RowCol(-1, 0))); // beyond top border 134 } 135 136 /** 137 * Get the tile at a given position in the grid. 138 * The coord must be in bounds. 139 * 140 * Params: 141 * coord = a row/column pair identifying a point in the tile grid. 142 */ 143 ref auto tileAt(RowCol coord) { 144 assert(this.contains(coord), "coord %s not in bounds".format(coord)); 145 return _tiles[coord.row][coord.col]; 146 } 147 148 /// 149 unittest { 150 auto grid = rectGrid([ 151 [ 00, 01, 02, 03, 04 ], 152 [ 10, 11, 12, 13, 14 ], 153 [ 20, 21, 22, 23, 24 ], 154 ]); 155 156 assert(grid.tileAt(RowCol(0, 0)) == 00); // top left tile 157 assert(grid.tileAt(RowCol(2, 4)) == 24); // bottom right tile 158 assert(grid.tileAt(RowCol(1, 1)) == 11); // one down/right from the top left 159 160 // tileAt returns a reference: 161 grid.tileAt(RowCol(2,2)) = 99; 162 assert(grid.tileAt(RowCol(2,2)) == 99); 163 } 164 165 /** 166 * Access tiles via a range of coords. Tiles are returned by ref. 167 * 168 * Params: 169 * coords = A range that yields coords. 170 */ 171 ref auto tilesAt(R)(R coords) if (isInputRange!R && is(typeof(coords.front) : RowCol)) { 172 alias GridType = typeof(this); 173 174 struct Result { 175 private GridType _grid; 176 private R _coords; 177 178 ref auto front() { return _grid.tileAt(_coords.front); } 179 bool empty() { return _coords.empty(); } 180 void popFront() { _coords.popFront(); } 181 182 static if (isForwardRange!R) { 183 auto save() { return Result(_grid, _coords.save); } 184 } 185 186 static if (isBidirectionalRange!R) { 187 auto back() { return _grid.tileAt(_coords.back); } 188 void popBack() { _coords.popBack(); } 189 } 190 } 191 192 return Result(this, coords); 193 } 194 195 unittest { 196 import std.range; 197 import std.algorithm : equal; 198 199 auto grid = rectGrid([ 200 [ 00, 01, 02, 03, 04 ], 201 [ 10, 11, 12, 13, 14 ], 202 [ 20, 21, 22, 23, 24 ], 203 ]); 204 205 auto r1 = chain(RowCol(0,0).only, RowCol(2,2).only, RowCol(2,0).only); 206 assert(grid.tilesAt(r1).equal([00, 22, 20])); 207 208 auto r2 = grid.allCoords.filter!(x => x.row > 1 && x.col > 2); 209 auto tiles = grid.tilesAt(r2); 210 assert(tiles.equal([23, 24])); 211 assert(tiles.equal([23, 24])); 212 } 213 214 /** 215 * Get a range that iterates through every coordinate in the grid. 216 */ 217 auto allCoords() { 218 return RowCol(0,0).span(RowCol(this.numRows, this.numCols)); 219 } 220 221 /// Use allCoords to apply range-oriented functions to the coords in the grid. 222 unittest { 223 import std.algorithm; 224 225 auto myGrid = rectGrid([ 226 [ 00, 01, 02, 03, 04 ], 227 [ 10, 11, 12, 13, 14 ], 228 [ 20, 21, 22, 23, 24 ], 229 ]); 230 231 auto coords = myGrid.allCoords 232 .filter!(x => x.col > 3) 233 .map!(x => x.row * 10 + x.col); 234 235 assert(coords.equal([04, 14, 24])); 236 } 237 238 /** 239 * Get a range that iterates through every tile in the grid. 240 */ 241 auto allTiles() { 242 return tilesAt(this.allCoords); 243 } 244 245 /// Use allTiles to apply range-oriented functions to the tiles in the grid. 246 unittest { 247 import std.algorithm; 248 249 auto myGrid = rectGrid([ 250 [ 00, 01, 02, 03, 04 ], 251 [ 10, 11, 12, 13, 14 ], 252 [ 20, 21, 22, 23, 24 ], 253 ]); 254 255 assert(myGrid.allTiles.filter!(x => x > 22).equal([23, 24])); 256 257 // use ref with allTiles to apply modifications 258 foreach(ref tile ; myGrid.allTiles.filter!(x => x < 10)) { 259 tile += 10; 260 } 261 262 assert(myGrid.tileAt(RowCol(0,0)) == 10); 263 } 264 265 /// Foreach over every tile in the grid. Supports `ref`. 266 int opApply(int delegate(ref TileType) fn) { 267 int res = 0; 268 269 foreach(coord ; this.allCoords) { 270 res = fn(this.tileAt(coord)); 271 if (res) break; 272 } 273 274 return res; 275 } 276 277 /// foreach with coords 278 unittest { 279 auto myGrid = rectGrid([ 280 [ 00, 01, 02, 03, 04 ], 281 [ 10, 11, 12, 13, 14 ], 282 [ 20, 21, 22, 23, 24 ], 283 ]); 284 285 int[] actual; 286 foreach(tile ; myGrid) { actual ~= tile; } 287 288 assert(actual == [ 289 00, 01, 02, 03, 04, 290 10, 11, 12, 13, 14, 291 20, 21, 22, 23, 24]); 292 } 293 294 /// Foreach over every (coord,tile) pair in the grid. Supports `ref`. 295 int opApply(int delegate(RowCol, ref TileType) fn) { 296 int res = 0; 297 298 foreach(coord ; this.allCoords) { 299 res = fn(coord, this.tileAt(coord)); 300 if (res) break; 301 } 302 303 return res; 304 } 305 306 /// foreach with coords 307 unittest { 308 auto myGrid = rectGrid([ 309 [ 00, 01, 02, 03, 04 ], 310 [ 10, 11, 12, 13, 14 ], 311 [ 20, 21, 22, 23, 24 ], 312 ]); 313 314 foreach(coord, tile ; myGrid) { 315 assert(tile == coord.row * 10 + coord.col); 316 } 317 } 318 319 /** 320 * Same as maskTiles, but return coords instead of tiles. 321 * 322 * Params: 323 * offset = map coordinate on which to align the top-left corner of the mask. 324 * mask = a rectangular array of true/false values indicating which tiles to take. 325 * each true value takes the tile at that grid coordinate. 326 * the mask should be in row major order (indexed as mask[row][col]). 327 */ 328 auto maskCoords(T)(RowCol offset, in T mask) if (isValidMask!T) { 329 assertNotJagged(mask, "mask cannot be a jagged array"); 330 331 return RowCol(0,0).span(arraySize2D(mask)) 332 .filter!(x => mask[x.row][x.col]) // remove elements that are 0 in the mask 333 .map!(x => x + offset) // add the offset to get the corresponding map coord 334 .filter!(x => this.contains(x)); // remove coords outside of bounds 335 } 336 337 /** 338 * Select specific tiles from this slice based on a mask. 339 * 340 * The upper left corner of the mask is positioned at the given offset. 341 * Each map tile that is overlaid with a 'true' value is included in the result. 342 * The mask is allowed to extend out of bounds - out of bounds coordinates are ignored 343 * 344 * Params: 345 * offset = map coordinate on which to align the top-left corner of the mask. 346 * mask = a rectangular array of true/false values indicating which tiles to take. 347 * each true value takes the tile at that grid coordinate. 348 * the mask should be in row major order (indexed as mask[row][col]). 349 * 350 * Examples: 351 * Suppose you are making a strategy game, and an attack hits all tiles in a cross pattern. 352 * This attack is used on the tile at row 2, column 3. 353 * You want to check each tile that was affected to see if any unit was hit: 354 * -------------- 355 * // cross pattern 356 * ubyte[][] attackPattern = [ 357 * [0,1,0] 358 * [1,1,1] 359 * [0,1,0] 360 * ]; 361 * 362 * // get tiles contained by a cross pattern centered at (2,3) 363 * auto tilesHit = map.maskTilesAround((RowCol(2, 3), attackPattern)); 364 * 365 * // now do something with those tiles 366 * auto unitsHit = tilesHit.map!(tile => tile.unitOnTile).filter!(unit => unit !is null); 367 * foreach(unit ; unitsHit) unit.applySomeEffect; 368 * -------------- 369 */ 370 auto maskTiles(T)(RowCol offset, in T mask) if (isValidMask!T) { 371 return this.maskCoords(mask).map!(x => this.tileAt(x)); 372 } 373 374 /** 375 * Same as maskCoords, but centered. 376 * 377 * Params: 378 * center = map coord on which to position the center of the mask. 379 * if the mask has an even side length, rounds down to compute the 'center' 380 * mask = a rectangular array of true/false values indicating which tiles to take. 381 * each true value takes the tile at that grid coordinate. 382 * the mask should be in row major order (indexed as mask[row][col]). 383 */ 384 auto maskCoordsAround(T)(RowCol center, in T mask) if (isValidMask!T) { 385 assertNotJagged(mask, "mask"); 386 387 auto offset = center - RowCol(mask.length / 2, mask[0].length / 2); 388 389 return this.maskCoords(offset, mask); 390 } 391 392 /** 393 * Same as maskTiles, but centered. 394 * 395 * Params: 396 * center = map coord on which to position the center of the mask. 397 * if the mask has an even side length, rounds down to compute the 'center' 398 * mask = a rectangular array of true/false values indicating which tiles to take. 399 * each true value takes the tile at that grid coordinate. 400 * the mask should be in row major order (indexed as mask[row][col]). 401 */ 402 auto maskTilesAround(T)(RowCol center, in T mask) if (isValidMask!T) { 403 return this.maskCoordsAround(center, mask).map!(x => this.tileAt(x)); 404 } 405 406 /// More masking examples: 407 unittest { 408 import std.array : empty; 409 import std.algorithm : equal; 410 411 auto myGrid = rectGrid([ 412 [ 00, 01, 02, 03, 04 ], 413 [ 10, 11, 12, 13, 14 ], 414 [ 20, 21, 22, 23, 24 ], 415 ]); 416 417 uint[3][3] mask1 = [ 418 [ 1, 1, 1 ], 419 [ 0, 0, 0 ], 420 [ 0, 0, 0 ], 421 ]; 422 assert(myGrid.maskTilesAround(RowCol(0,0), mask1).empty); 423 assert(myGrid.maskTilesAround(RowCol(1,1), mask1).equal([00, 01, 02])); 424 assert(myGrid.maskTilesAround(RowCol(2,1), mask1).equal([10, 11, 12])); 425 assert(myGrid.maskTilesAround(RowCol(2,4), mask1).equal([13, 14])); 426 427 auto mask2 = [ 428 [ 0, 0, 1 ], 429 [ 0, 0, 1 ], 430 [ 1, 1, 1 ], 431 ]; 432 assert(myGrid.maskTilesAround(RowCol(0,0), mask2).equal([01, 10, 11])); 433 assert(myGrid.maskTilesAround(RowCol(1,2), mask2).equal([03, 13, 21, 22, 23])); 434 assert(myGrid.maskTilesAround(RowCol(2,4), mask2).empty); 435 436 auto mask3 = [ 437 [ 0 , 0 , 1 , 0 , 0 ], 438 [ 1 , 0 , 1 , 0 , 1 ], 439 [ 0 , 0 , 0 , 0 , 0 ], 440 ]; 441 assert(myGrid.maskTilesAround(RowCol(0,0), mask3).equal([00, 02])); 442 assert(myGrid.maskTilesAround(RowCol(1,2), mask3).equal([02, 10, 12, 14])); 443 444 auto mask4 = [ 445 [ 1 , 1 , 1 , 0 , 1 ], 446 ]; 447 assert(myGrid.maskTilesAround(RowCol(0,0), mask4).equal([00, 02])); 448 assert(myGrid.maskTilesAround(RowCol(2,2), mask4).equal([20, 21, 22, 24])); 449 450 auto mask5 = [ 451 [ 1 ], 452 [ 1 ], 453 [ 0 ], 454 [ 1 ], 455 [ 1 ], 456 ]; 457 assert(myGrid.maskTilesAround(RowCol(0,4), mask5).equal([14, 24])); 458 assert(myGrid.maskTilesAround(RowCol(1,1), mask5).equal([01, 21])); 459 } 460 461 /** 462 * Return all tiles adjacent to the tile at the given coord (not including the tile itself). 463 * 464 * Params: 465 * coord = grid location of center tile. 466 * diagonals = if no, include tiles to the north, south, east, and west only. 467 * if yes, also include northwest, northeast, southwest, and southeast. 468 */ 469 auto adjacentTiles(RowCol coord, Diagonals diagonals = Diagonals.no) { 470 return coord.adjacent(diagonals) 471 .filter!(x => this.contains(x)) 472 .map!(x => this.tileAt(x)); 473 } 474 475 /// 476 unittest { 477 import std.algorithm : equal; 478 auto myGrid = rectGrid([ 479 [ 00, 01, 02, 03, 04 ], 480 [ 10, 11, 12, 13, 14 ], 481 [ 20, 21, 22, 23, 24 ], 482 ]); 483 484 assert(myGrid.adjacentTiles(RowCol(0,0)).equal([01, 10])); 485 assert(myGrid.adjacentTiles(RowCol(1,1)).equal([01, 10, 12, 21])); 486 assert(myGrid.adjacentTiles(RowCol(2,2)).equal([12, 21, 23])); 487 assert(myGrid.adjacentTiles(RowCol(2,4)).equal([14, 23])); 488 489 assert(myGrid.adjacentTiles(RowCol(0,0), Diagonals.yes) 490 .equal([01, 10, 11])); 491 assert(myGrid.adjacentTiles(RowCol(1,1), Diagonals.yes) 492 .equal([00, 01, 02, 10, 12, 20, 21, 22])); 493 assert(myGrid.adjacentTiles(RowCol(2,2), Diagonals.yes) 494 .equal([11, 12, 13, 21, 23])); 495 assert(myGrid.adjacentTiles(RowCol(2,4), Diagonals.yes) 496 .equal([13, 14, 23])); 497 } 498 } 499 500 // NOTE: declared outside of struct due to issues with alias parameters on templated structs. 501 // See https://issues.dlang.org/show_bug.cgi?id=11098 502 /** 503 * Generate a mask from a region of tiles based on a condition. 504 * 505 * For each tile in the grid, sets the corresponding element of mask to the result of fn(tile). 506 * If a coordinate is out of bounds (e.g. if you are generating a mask from a slice that extends 507 * over the map border) the mask value is the init value of the mask's element type. 508 * 509 * Params: 510 * fn = function that generates a mask entry from a tile 511 * grid = grid to generate mask from 512 * offset = map coord from which to start the top-left corner of the mask 513 * mask = rectangular array to populate with generated mask values. 514 * must match the size of the grid 515 */ 516 void createMask(alias fn, T, U)(T grid, RowCol offset, ref U mask) 517 if(__traits(compiles, { mask[0][0] = fn(grid.tileAt(RowCol(0,0))); })) 518 { 519 assertNotJagged(mask, "mask"); 520 521 foreach(coord ; RowCol(0,0).span(arraySize2D(mask))) { 522 auto mapCoord = coord + offset; 523 524 mask[coord.row][coord.col] = (grid.contains(mapCoord)) ? 525 fn(grid.tileAt(mapCoord)) : // in bounds, apply fn to generate mask value 526 typeof(mask[0][0]).init; // out of bounds, use default value 527 } 528 } 529 530 /** 531 * Same as createMask, but specify the offset of the mask's center rather than the top-left corner. 532 * 533 * Params: 534 * fn = function that generates a mask entry from a tile 535 * grid = grid to generate mask from 536 * center = center position around which to generate mask 537 * mask = rectangular array to populate with generated mask values. 538 * must match the size of the grid 539 */ 540 void createMaskAround(alias fn, T, U)(T grid, RowCol center, ref U mask) 541 if(__traits(compiles, { mask[0][0] = fn(grid.tileAt(RowCol(0,0))); })) 542 { 543 assertNotJagged(mask, "mask"); 544 545 auto offset = center - RowCol(mask.length / 2, mask[0].length / 2); 546 grid.createMask!fn(offset, mask); 547 } 548 549 /// 550 unittest { 551 auto myGrid = rectGrid([ 552 [ 00, 01, 02, 03, 04 ], 553 [ 10, 11, 12, 13, 14 ], 554 [ 20, 21, 22, 23, 24 ], 555 ]); 556 557 uint[3][3] mask; 558 559 myGrid.createMaskAround!(tile => tile > 10)(RowCol(1,1), mask); 560 561 assert(mask == [ 562 [0, 0, 0], 563 [0, 1, 1], 564 [1, 1, 1], 565 ]); 566 567 myGrid.createMaskAround!(tile => tile < 24)(RowCol(2,4), mask); 568 569 assert(mask == [ 570 [1, 1, 0], 571 [1, 0, 0], 572 [0, 0, 0], 573 ]); 574 } 575 576 private: 577 // assertion helper for input array args 578 void assertNotJagged(T)(in T array, string msg) { 579 assert(array[].all!(x => x.length == array[0].length), msg); 580 } 581 582 // get a RowCol representing the size of a 2D array (assumed non-jagged). 583 auto arraySize2D(T)(in T array) { 584 return RowCol(array.length, array[0].length); 585 } 586 587 enum isValidMask(T) = is(typeof(cast(bool) T.init[0][0])) && // must have boolean elements 588 is(typeof(T.init.length) : size_t) && // must have row count 589 is(typeof(T.init[0].length) : size_t); // must have column count 590 591 unittest { 592 static assert(isValidMask!(int[][])); 593 static assert(isValidMask!(uint[][3])); 594 static assert(isValidMask!(uint[3][])); 595 static assert(isValidMask!(uint[3][3])); 596 597 static assert(!isValidMask!int); 598 static assert(!isValidMask!(int[])); 599 }