001package squidpony.squidgrid.mapping; 002 003 004import squidpony.squidgrid.Direction; 005import squidpony.squidmath.Coord; 006import squidpony.squidmath.GWTRNG; 007import squidpony.squidmath.IRNG; 008import squidpony.squidmath.OrderedSet; 009 010import java.util.ArrayDeque; 011import java.util.ArrayList; 012import java.util.Arrays; 013 014/** 015 * Creates a dungeon in the style of the original Rogue game. It will always 016 * make a grid style of rooms where there are a certain number horizontally and 017 * vertically and it will link them only next to each other. 018 * 019 * This dungeon generator is based on a port of the rot.js version. 020 * 021 * @author hyakugei 022 * @author Eben Howard - http://squidpony.com - howard@squidpony.com 023 */ 024public class ClassicRogueMapGenerator implements IDungeonGenerator{ 025 026 /** 027 * Holds the information needed to track rooms in the classic rogue 028 * generation algorithm. 029 */ 030 private static class ClassicRogueRoom { 031 032 private int x, y, width, height, cellx, celly; 033 private final OrderedSet<ClassicRogueRoom> connections = new OrderedSet<>(4); 034 ClassicRogueRoom(int x, int y, int width, int height, int cellx, int celly) { 035 this.x = x; 036 this.y = y; 037 this.width = width; 038 this.height = height; 039 this.cellx = cellx; 040 this.celly = celly; 041 } 042 043 @Override 044 public int hashCode() { 045 return Coord.xoroHashCode(cellx, celly); 046 // the actual algorithm doesn't matter much here. 047 // another good option, if you don't feel like looking up xoroHashCode(), is 048 //return (int)(0xC13FA9A902A6328FL * cellx + 0x91E10DA5C79E7B1DL * celly >>> 32); 049 } 050 051 @Override 052 public boolean equals(Object obj) { 053 if (obj == null) { 054 return false; 055 } 056 if (getClass() != obj.getClass()) { 057 return false; 058 } 059 final ClassicRogueRoom other = (ClassicRogueRoom) obj; 060 if (cellx != other.cellx) { 061 return false; 062 } 063 return celly == other.celly; 064 } 065 } 066 067 private IRNG rng; 068 069 private int horizontalRooms, verticalRooms, dungeonWidth, dungeonHeight, 070 minRoomWidth, maxRoomWidth, minRoomHeight, maxRoomHeight; 071 private ClassicRogueRoom[][] rooms; 072 private char[][] dungeon; 073 private Direction[] dirToCheck = new Direction[4]; 074 /** 075 * Initializes the generator to turn out random dungeons within the specific 076 * parameters. 077 * 078 * Will size down the room width and height parameters if needed to ensure 079 * the desired number of rooms will fit both horizontally and vertically. 080 * 081 * @param horizontalRooms How many rooms will be made horizontally 082 * @param verticalRooms How many rooms will be made vertically 083 * @param dungeonWidth How wide the total dungeon will be 084 * @param dungeonHeight How high the total dungeon will be 085 * @param minRoomWidth The minimum width a room can be 086 * @param maxRoomWidth The maximum width a room can be 087 * @param minRoomHeight The minimum height a room can be 088 * @param maxRoomHeight The maximum height a room can be 089 */ 090 public ClassicRogueMapGenerator(int horizontalRooms, int verticalRooms, int dungeonWidth, int dungeonHeight, 091 int minRoomWidth, int maxRoomWidth, int minRoomHeight, int maxRoomHeight) { 092 this(horizontalRooms, verticalRooms, dungeonWidth, dungeonHeight, minRoomWidth, maxRoomWidth, minRoomHeight, maxRoomHeight, new GWTRNG()); 093 } 094 095 /** 096 * Initializes the generator to turn out random dungeons within the specific 097 * parameters. 098 * 099 * Will size down the room width and height parameters if needed to ensure 100 * the desired number of rooms will fit both horizontally and vertically. 101 * 102 * @param horizontalRooms How many rooms will be made horizontally 103 * @param verticalRooms How many rooms will be made vertically 104 * @param dungeonWidth How wide the total dungeon will be 105 * @param dungeonHeight How high the total dungeon will be 106 * @param minRoomWidth The minimum width a room can be 107 * @param maxRoomWidth The maximum width a room can be 108 * @param minRoomHeight The minimum height a room can be 109 * @param maxRoomHeight The maximum height a room can be 110 */ 111 public ClassicRogueMapGenerator(int horizontalRooms, int verticalRooms, int dungeonWidth, int dungeonHeight, 112 int minRoomWidth, int maxRoomWidth, int minRoomHeight, int maxRoomHeight, IRNG rng) 113 { 114 this.rng = rng; 115 this.horizontalRooms = horizontalRooms; 116 this.verticalRooms = verticalRooms; 117 this.dungeonWidth = dungeonWidth; 118 this.dungeonHeight = dungeonHeight; 119 this.minRoomWidth = minRoomWidth; 120 this.maxRoomWidth = maxRoomWidth; 121 this.minRoomHeight = minRoomHeight; 122 this.maxRoomHeight = maxRoomHeight; 123 124 sanitizeRoomDimensions(); 125 } 126 127 private void sanitizeRoomDimensions() { 128 int test = (dungeonWidth - 3 * horizontalRooms) / horizontalRooms;//have to leave space for hallways 129 maxRoomWidth = Math.min(test, maxRoomWidth); 130 minRoomWidth = Math.max(minRoomWidth, 2); 131 minRoomWidth = Math.min(minRoomWidth, maxRoomWidth); 132 133 test = (dungeonHeight - 3 * verticalRooms) / verticalRooms;//have to leave space for hallways 134 maxRoomHeight = Math.min(test, maxRoomHeight); 135 minRoomHeight = Math.max(minRoomHeight, 2); 136 minRoomHeight = Math.min(minRoomHeight, maxRoomHeight); 137 } 138 139 /** 140 * Builds and returns a map in the Classic Rogue style, returned as a 2D char array. 141 * 142 * Only includes rooms ('.' for floor and '#' for walls), corridors (using the same chars as rooms) and doors ('+' 143 * for closed doors, does not generate open doors). 144 * @return a 2D char array version of the map 145 */ 146 public char[][] generate() 147 { 148 initRooms(); 149 connectRooms(); 150 connectUnconnectedRooms(); 151 fullyConnect(); 152 createRooms(); 153 createCorridors(); 154 return dungeon; 155 } 156 157 public char[][] getDungeon() { 158 return dungeon; 159 } 160 161 private void initRooms() { 162 rooms = new ClassicRogueRoom[horizontalRooms][verticalRooms]; 163 dungeon = new char[dungeonWidth][dungeonHeight]; 164 for (int x = 0; x < horizontalRooms; x++) { 165 for (int y = 0; y < verticalRooms; y++) { 166 rooms[x][y] = new ClassicRogueRoom(0, 0, 0, 0, x, y); 167 } 168 } 169 for (int x = 0; x < dungeonWidth; x++) { 170 for (int y = 0; y < dungeonHeight; y++) { 171 dungeon[x][y] = '#'; 172 } 173 } 174 } 175 176 private void connectRooms() { 177 ArrayList<ClassicRogueRoom> unconnected = new ArrayList<>(); 178 for (int x = 0; x < horizontalRooms; x++) { 179 unconnected.addAll(Arrays.asList(rooms[x]).subList(0, verticalRooms)); 180 } 181 rng.shuffleInPlace(unconnected); 182 183 for (int i = 0; i < unconnected.size(); i++) { 184 ClassicRogueRoom room = unconnected.get(i); 185 rng.shuffle(Direction.CARDINALS, dirToCheck); 186 for (Direction dir : dirToCheck) { 187 int nextX = room.x + dir.deltaX; 188 int nextY = room.y + dir.deltaY; 189 if (nextX < 0 || nextX >= horizontalRooms || nextY < 0 || nextY >= verticalRooms) { 190 continue; 191 } 192 ClassicRogueRoom otherRoom = rooms[nextX][nextY]; 193 194 if (room.connections.contains(otherRoom)) { 195 break;//already connected to this room 196 } 197 198 if (!otherRoom.connections.isEmpty()) { 199 room.connections.add(otherRoom); 200 break; 201 } 202 } 203 } 204 } 205 206 private void connectUnconnectedRooms() { 207 for (int x = 0; x < horizontalRooms; x++) { 208 for (int y = 0; y < verticalRooms; y++) { 209 ClassicRogueRoom room = rooms[x][y]; 210 211 if (room.connections.isEmpty()) { 212 rng.shuffle(Direction.CARDINALS, dirToCheck); 213 214 boolean validRoom = false; 215 ClassicRogueRoom otherRoom = null; 216 int idx = 0; 217 do { 218 Direction dir = dirToCheck[idx++]; 219 220 int nextX = x + dir.deltaX; 221 if (nextX < 0 || nextX >= horizontalRooms) { 222 continue; 223 } 224 int nextY = y + dir.deltaY; 225 if (nextY < 0 || nextY >= verticalRooms) { 226 continue; 227 } 228 229 otherRoom = rooms[nextX][nextY]; 230 validRoom = true; 231 232 if (otherRoom.connections.contains(room)) { 233 validRoom = false; 234 } 235 else { 236 break; 237 } 238 } while (idx < 4); 239 240 if (validRoom) { 241 room.connections.add(otherRoom); 242 } 243 } 244 } 245 } 246 } 247 248 private void fullyConnect() { 249 boolean allGood; 250 do { 251 ArrayDeque<ClassicRogueRoom> deq = new ArrayDeque<>(horizontalRooms * verticalRooms); 252 for (int x = 0; x < horizontalRooms; x++) { 253 for (int y = 0; y < verticalRooms; y++) { 254 deq.offer(rooms[x][y]); 255 } 256 } 257 ArrayList<ClassicRogueRoom> connected = new ArrayList<>(); 258 connected.add(deq.removeFirst()); 259 boolean changed = true; 260 TESTING: 261 while (changed) { 262 changed = false; 263 for (ClassicRogueRoom test : deq) { 264 for (int i = 0, connectedSize = connected.size(); i < connectedSize; i++) { 265 ClassicRogueRoom r = connected.get(i); 266 if (test.connections.contains(r) || r.connections.contains(test)) { 267 connected.add(test); 268 deq.remove(test); 269 changed = true; 270 continue TESTING; 271 } 272 } 273 } 274 } 275 276 allGood = true; 277 if (!deq.isEmpty()) { 278 TESTING: 279 for (ClassicRogueRoom room : deq) { 280 for (Direction dir : Direction.CARDINALS) { 281 int x = room.cellx + dir.deltaX; 282 int y = room.celly + dir.deltaY; 283 if (x >= 0 && y >= 0 && x < horizontalRooms && y < verticalRooms) { 284 ClassicRogueRoom otherRoom = rooms[x][y]; 285 if (connected.contains(otherRoom)) { 286 room.connections.add(otherRoom); 287 allGood = false; 288 break TESTING; 289 } 290 } 291 } 292 } 293 } 294 295 } while (!allGood); 296 } 297 298 private void createRooms() { 299 int cwp = dungeonWidth / horizontalRooms; 300 int chp = dungeonHeight / verticalRooms; 301 302 ClassicRogueRoom otherRoom; 303 304 for (int x = 0; x < horizontalRooms; x++) { 305 for (int y = 0; y < verticalRooms; y++) { 306 int sx = cwp * x; 307 int sy = chp * y; 308 309 sx = Math.max(sx, 2); 310 sy = Math.max(sy, 2); 311 312 int roomw = rng.between(minRoomWidth, maxRoomWidth + 1); 313 int roomh = rng.between(minRoomHeight, maxRoomHeight + 1); 314 315 if (y > 0) { 316 otherRoom = rooms[x][y - 1]; 317 while (sy - (otherRoom.y + otherRoom.height) < 3) { 318 sy++; 319 } 320 } 321 322 if (x > 0) { 323 otherRoom = rooms[x - 1][y]; 324 while (sx - (otherRoom.x + otherRoom.width) < 3) { 325 sx++; 326 } 327 } 328 329 int sxOffset = Math.round(rng.nextInt(cwp - roomw) * 0.5f); 330 int syOffset = Math.round(rng.nextInt(chp - roomh) * 0.5f); 331 332 while (sx + sxOffset + roomw >= dungeonWidth) { 333 if (sxOffset > 0) { 334 sxOffset--; 335 } else { 336 roomw--; 337 } 338 } 339 while (sy + syOffset + roomh >= dungeonHeight) { 340 if (syOffset > 0) { 341 syOffset--; 342 } else { 343 roomh--; 344 } 345 } 346 347 sx += sxOffset; 348 sy += syOffset; 349 350 ClassicRogueRoom r = rooms[x][y]; 351 r.x = sx; 352 r.y = sy; 353 r.width = roomw; 354 r.height = roomh; 355 356 for (int xx = sx; xx < sx + roomw; xx++) { 357 for (int yy = sy; yy < sy + roomh; yy++) { 358 dungeon[xx][yy] = '.'; 359 } 360 } 361 } 362 } 363 } 364 365 private Coord randomWallPosition(ClassicRogueRoom room, Direction dir) { 366 int x, y; 367 Coord p = null; 368 369 switch (dir) { 370 case LEFT: 371 y = rng.between(room.y + 1, room.y + room.height); 372 x = room.x - 1; 373 dungeon[x][y] = '+'; 374 p = Coord.get(x - 1, y); 375 break; 376 case RIGHT: 377 y = rng.between(room.y + 1, room.y + room.height); 378 x = room.x + room.width; 379 dungeon[x][y] = '+'; 380 p = Coord.get(x + 1, y); 381 break; 382 case DOWN: 383 x = rng.between(room.x + 1, room.x + room.width); 384 y = room.y - 1; 385 dungeon[x][y] = '+'; 386 p = Coord.get(x, y + 1); 387 break; 388 case UP: 389 x = rng.between(room.x + 1, room.x + room.width); 390 y = room.y + room.height; 391 dungeon[x][y] = '+'; 392 p = Coord.get(x, y - 1); 393 break; 394 case NONE: 395 break; 396 case DOWN_LEFT: 397 case DOWN_RIGHT: 398 case UP_LEFT: 399 case UP_RIGHT: 400 throw new IllegalStateException("There should only be cardinal positions here"); 401 } 402 403 return p; 404 } 405 406 /** 407 * Draws a corridor between the two points with a zig-zag in between. 408 * 409 * @param start 410 * @param end 411 */ 412 private void digPath(Coord start, Coord end) { 413 int xOffset = end.x - start.x; 414 int yOffset = end.y - start.y; 415 int xpos = start.x; 416 int ypos = start.y; 417 418 ArrayDeque<Magnitude> moves = new ArrayDeque<>(); 419 420 int xAbs = Math.abs(xOffset); 421 int yAbs = Math.abs(yOffset); 422 423 double firstHalf = rng.nextDouble(); 424 double secondHalf = 1 - firstHalf; 425 426 Direction xDir = xOffset < 0 ? Direction.LEFT : Direction.RIGHT; 427 Direction yDir = yOffset > 0 ? Direction.DOWN : Direction.UP; 428 429 if (xAbs < yAbs) { 430 int tempDist = (int) Math.ceil(yAbs * firstHalf); 431 moves.add(new Magnitude(yDir, tempDist)); 432 moves.add(new Magnitude(xDir, xAbs)); 433 tempDist = (int) Math.floor(yAbs * secondHalf); 434 moves.add(new Magnitude(yDir, tempDist)); 435 } else { 436 int tempDist = (int) Math.ceil(xAbs * firstHalf); 437 moves.add(new Magnitude(xDir, tempDist)); 438 moves.add(new Magnitude(yDir, yAbs)); 439 tempDist = (int) Math.floor(xAbs * secondHalf); 440 moves.add(new Magnitude(xDir, tempDist)); 441 } 442 443 dungeon[xpos][ypos] = '.'; 444 445 while (!moves.isEmpty()) { 446 Magnitude move = moves.removeFirst(); 447 Direction dir = move.dir; 448 int dist = move.distance; 449 while (dist > 0) { 450 xpos += dir.deltaX; 451 ypos += dir.deltaY; 452 dungeon[xpos][ypos] = '.'; 453 dist--; 454 } 455 } 456 } 457 458 private void createCorridors() { 459 for (int x = 0; x < horizontalRooms; x++) { 460 for (int y = 0; y < verticalRooms; y++) { 461 ClassicRogueRoom room = rooms[x][y]; 462 for (int r = 0; r < room.connections.size(); r++) { 463 ClassicRogueRoom otherRoom = room.connections.getAt(r); 464 Direction dir = Direction.getCardinalDirection(otherRoom.cellx - room.cellx, otherRoom.celly - room.celly); 465 digPath(randomWallPosition(room, dir), randomWallPosition(otherRoom, dir.opposite())); 466 } 467 } 468 } 469 } 470 471 private static class Magnitude { 472 473 public Direction dir; 474 public int distance; 475 476 public Magnitude(Direction dir, int distance) { 477 this.dir = dir; 478 this.distance = distance; 479 } 480 } 481}