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}