com.callidusrobotics.locale.DungeonBuilder.java Source code

Java tutorial

Introduction

Here is the source code for com.callidusrobotics.locale.DungeonBuilder.java

Source

/**
 * Copyright (C) 2013 Rusty Gerard
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package com.callidusrobotics.locale;

import java.awt.Color;
import java.io.IOException;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;

import org.apache.commons.codec.binary.Base64;
import org.apache.commons.io.IOUtils;

import com.callidusrobotics.ConsoleColleague;
import com.callidusrobotics.object.actor.PlayerCharacter;
import com.callidusrobotics.util.XmlMarshaller;

@SuppressWarnings("PMD.TooManyMethods")
public final class DungeonBuilder {
    private final DungeonMapData dungeonMapData;
    private List<Room> rooms;
    private final List<Room> specialRooms = new LinkedList<Room>();
    private final List<Direction> specialRoomExits = new LinkedList<Direction>();
    private int specialRoomCount = 0;

    public DungeonBuilder(final String xmlFile) throws IOException {
        final XmlMarshaller xmlMarshaller = new XmlMarshaller(DungeonMapData.class);
        dungeonMapData = (DungeonMapData) xmlMarshaller
                .unMarshal(IOUtils.toString(DungeonBuilder.class.getResourceAsStream(xmlFile)));
    }

    public DungeonParameters getDungeonParameters() {
        return dungeonMapData.getDungeonParameters();
    }

    public List<Room> getRooms() {
        return rooms;
    }

    public List<Room> getSpecialRooms() {
        return specialRooms;
    }

    public Room buildDungeon(final PlayerCharacter player) throws IOException {
        if (dungeonMapData.specialRoomFiles != null) {
            final XmlMarshaller xmlMarshaller = new XmlMarshaller(DungeonMapData.class);

            for (final String xmlFile : dungeonMapData.specialRoomFiles) {
                final DungeonMapData roomBuilder = (DungeonMapData) xmlMarshaller
                        .unMarshal(IOUtils.toString(DungeonBuilder.class.getResourceAsStream(xmlFile)));
                final Room specialRoom = roomBuilder.buildSpecialRoom(player);
                if (specialRoom != null && roomBuilder.exit != null) {
                    specialRoom.setPosition(ConsoleColleague.getPosition(specialRoom));
                    specialRoomExits.add(roomBuilder.exit);
                    specialRooms.add(specialRoom);
                }
            }
        }

        final Room entrance = dungeonMapData.buildSpecialRoom(player);
        entrance.setPosition(ConsoleColleague.getPosition(entrance));

        if (dungeonMapData.dungeonParameters != null) {
            final String serializedDungeon = buildRandomDungeonString(dungeonMapData.dungeonParameters, entrance,
                    dungeonMapData.exit);
            buildDungeonFromString(entrance, dungeonMapData.exit, serializedDungeon,
                    dungeonMapData.dungeonParameters);
        }

        return entrance;
    }

    protected Room buildDungeonFromString(final Room entrance, final Direction direction,
            final String serializedDungeon, final DungeonParameters dungeonParameters) {
        final byte[] buffer = Base64.decodeBase64(serializedDungeon);

        final int numRooms = buffer.length / 4;
        rooms = new ArrayList<Room>(numRooms);

        for (int i = 0; i < numRooms; i++) {
            final RoomType roomType = getRoomType(buffer, i);
            final int height = getRoomHeight(buffer, i);
            final int width = getRoomWidth(buffer, i);

            rooms.add(RoomFactory.makeRoom(roomType, height, width, dungeonParameters.getForeground(),
                    dungeonParameters.getBackground()));
        }

        linkRooms(entrance, rooms.get(0), direction);
        linkRooms(buffer, rooms);

        // Attach the special rooms to the dungeon
        int collisionCount = 1;
        while (collisionCount++ < 1000 && specialRoomCount < specialRooms.size()) {
            final int index = rooms.size() - (1 + collisionCount % rooms.size());
            if (specialRoomCount < specialRooms.size()
                    && rooms.get(index).getNeighbor(specialRoomExits.get(specialRoomCount).opposite()) == null) {
                linkRooms(specialRooms.get(specialRoomCount), rooms.get(index),
                        specialRoomExits.get(specialRoomCount));
                specialRoomCount++;
            } else {
                collisionCount++;
            }
        }

        return rooms.get(0);
    }

    public String buildRandomDungeonString(final DungeonParameters dungeonParameters, final Room entrance,
            final Direction direction) {
        final int maxRooms = dungeonParameters.getMinRooms()
                + dungeonParameters.nextInt(dungeonParameters.getMaxRooms() - dungeonParameters.getMinRooms() + 1);

        // Build a list of placeholder rooms to generate the string from
        final List<Room> rooms = new ArrayList<Room>(maxRooms);
        final byte[] buffer = new byte[4 * maxRooms];

        for (int i = 0; i < maxRooms; i++) {
            // Just create a room as a placeholder; we'll replace it with the appropriate type later if necessary
            final Room room = RoomFactory.makeRandomRoom(dungeonParameters);

            rooms.add(room);

            int numNeighbors = 2 + dungeonParameters.nextInt(3);
            while (numNeighbors > 0) {
                final int index = dungeonParameters.nextInt(4);

                if (!getRoomExit(buffer, i, Direction.values()[index])) {
                    numNeighbors--;
                    setRoomExit(buffer, i, Direction.values()[index]);
                }
            }

            setRoomType(buffer, i, room.getRoomType());
            setRoomHeight(buffer, i, room.getHeight());
            setRoomWidth(buffer, i, room.getWidth());
        }

        // Set the root node of the spanning tree to have 4 exits
        buffer[3] = 0xF;

        // Link the rooms so that we can then re-map rooms with funky connections (e.g. cross intersections with fewer than 4 exits)
        linkRooms(entrance, rooms.get(0), direction);
        linkRooms(buffer, rooms);
        fixRoomTypes(buffer);

        // Clear the placeholder rooms
        rooms.clear();
        entrance.setNeighbor(null, direction);

        return Base64.encodeBase64String(buffer);
    }

    protected void linkRooms(final byte[] buffer, final List<Room> rooms) {
        for (int i = 0; i < rooms.size(); i++) {
            int neighborCount = 0;

            for (int j = 0; j < 4; j++) {
                int exitMask = (1 << j);
                if ((getRoomExits(buffer, i) & exitMask) != 0) {
                    neighborCount++;
                    int collisions = 0;
                    boolean linked = rooms.get(i).getNeighbor(Direction.values()[j]) != null;

                    exitMask = (1 << Direction.values()[j].opposite().ordinal());
                    int sanityCount = 0;
                    while (!linked && sanityCount++ < 1000) {
                        collisions++;
                        final int index = (i + neighborCount + collisions / 2 + collisions * collisions / 2)
                                % rooms.size();
                        linked = rooms.get(index).getNeighbor(Direction.values()[j].opposite()) == null
                                && (getRoomExits(buffer, index) & exitMask) != 0;

                        if (linked) {
                            linkRooms(rooms.get(i), rooms.get(index), Direction.values()[j]);
                        }
                    }
                }
            }
        }

        eliminateDanglingExits(buffer, rooms);
        fixOrphans(rooms);
    }

    protected void fixRoomTypes(final byte[] buffer) {
        final int maxRooms = buffer.length / 4;
        for (int i = 0; i < maxRooms; i++) {
            fixDeadEnds(buffer, i);
            fixVerticalHalls(buffer, i);
            fixHorizontalHalls(buffer, i);
            fixIntersections(buffer, i);
        }
    }

    private void fixDeadEnds(final byte[] buffer, final int roomNum) {
        final int numExits = getNumRoomExits(buffer, roomNum);
        final RoomType roomType = getRoomType(buffer, roomNum);
        final boolean isIntersection = roomType == RoomType.INTERSECTION;
        final boolean isHallway = roomType == RoomType.VERTICAL_HALL || roomType == RoomType.HORIZONTAL_HALL;

        if (numExits == 1 && (isIntersection || isHallway)) {
            final int dimension = Math.max(getRoomWidth(buffer, roomNum), getRoomHeight(buffer, roomNum));

            setRoomType(buffer, roomNum, RoomType.RECTANGLE);
            setRoomWidth(buffer, roomNum, dimension);
            setRoomHeight(buffer, roomNum, dimension);
        }
    }

    private void fixVerticalHalls(final byte[] buffer, final int roomNum) {
        final int numExits = getNumRoomExits(buffer, roomNum);
        final RoomType roomType = getRoomType(buffer, roomNum);
        final boolean hasNorthExit = getRoomExit(buffer, roomNum, Direction.NORTH);
        final boolean hasSouthExit = getRoomExit(buffer, roomNum, Direction.SOUTH);
        final boolean isIntersection = roomType == RoomType.INTERSECTION;
        final boolean isHorizontalHall = roomType == RoomType.HORIZONTAL_HALL;

        if (numExits == 2 && hasNorthExit && hasSouthExit && (isIntersection || isHorizontalHall)) {
            final int width = getRoomWidth(buffer, roomNum);
            final int height = getRoomHeight(buffer, roomNum);

            setRoomType(buffer, roomNum, RoomType.VERTICAL_HALL);
            setRoomWidth(buffer, roomNum, Math.min(width, height));
            setRoomHeight(buffer, roomNum, Math.max(width, height));
        }
    }

    private void fixHorizontalHalls(final byte[] buffer, final int roomNum) {
        final int numExits = getNumRoomExits(buffer, roomNum);
        final RoomType roomType = getRoomType(buffer, roomNum);
        final boolean hasEastExit = getRoomExit(buffer, roomNum, Direction.EAST);
        final boolean hasWestExit = getRoomExit(buffer, roomNum, Direction.WEST);
        final boolean isIntersection = roomType == RoomType.INTERSECTION;
        final boolean isVerticalHall = roomType == RoomType.VERTICAL_HALL;

        if (numExits == 2 && hasEastExit && hasWestExit && (isIntersection || isVerticalHall)) {
            final int width = getRoomWidth(buffer, roomNum);
            final int height = getRoomHeight(buffer, roomNum);

            setRoomType(buffer, roomNum, RoomType.HORIZONTAL_HALL);
            setRoomWidth(buffer, roomNum, Math.max(width, height));
            setRoomHeight(buffer, roomNum, Math.min(width, height));
        }
    }

    private void fixIntersections(final byte[] buffer, final int roomNum) {
        final int numExits = getNumRoomExits(buffer, roomNum);
        final RoomType roomType = getRoomType(buffer, roomNum);
        final boolean isHallway = roomType == RoomType.VERTICAL_HALL || roomType == RoomType.HORIZONTAL_HALL;

        if (numExits > 2 && isHallway) {
            final int dimension = Math.max(getRoomWidth(buffer, roomNum), getRoomHeight(buffer, roomNum));

            setRoomType(buffer, roomNum, RoomType.INTERSECTION);
            setRoomWidth(buffer, roomNum, dimension);
            setRoomHeight(buffer, roomNum, dimension);
        }
    }

    private void fixOrphans(final List<Room> rooms) {
        final List<Room> orphans = new ArrayList<Room>(rooms.size());
        final List<Room> siblings = new ArrayList<Room>(rooms.size());

        // Do a depth-first search of the rooms list and mark each room as visited
        depthFirstTraversal(rooms.get(0));

        // Add each unvisited room to the orphans list
        for (final Room room : rooms) {
            if (room.isVisited()) {
                siblings.add(room);
                room.setIsVisited(false);
            } else {
                orphans.add(room);
            }
        }

        // For each orphaned room, connect it to the first potential sibling
        // If the orphan is fully connected to other orphans, then proceed to the next orphan in the list
        // Note that there is an extremely low probability of orphans and this code usually is not executed
        for (final Room orphan : orphans) {
            boolean foundSibling = false;
            for (int i = 0; i < 4 && !foundSibling; i++) {
                final Direction exit = Direction.values()[i];
                final int oppositeIndex = exit.opposite().ordinal();

                if (orphan.neighbors[i] == null) {
                    for (int j = 0; j < siblings.size() && !foundSibling; j++) {
                        if (siblings.get(j).neighbors[oppositeIndex] == null) {
                            foundSibling = true;
                            linkRooms(orphan, siblings.get(j), exit);
                        }
                    }
                }
            }
        }
    }

    private void depthFirstTraversal(final Room currentRoom) {
        currentRoom.setIsVisited(true);

        for (final Room room : currentRoom.neighbors) {
            if (room != null && !room.isVisited()) {
                depthFirstTraversal(room);
            }
        }
    }

    private void eliminateDanglingExits(final byte[] buffer, final List<Room> rooms) {
        for (int i = 0; i < rooms.size(); i++) {
            for (int j = 0; j < 4; j++) {
                final int exitMask = (1 << j);
                final boolean linked = (getRoomExits(buffer, i) & exitMask) != 0
                        && rooms.get(i).getNeighbor(Direction.values()[j]) != null;

                if (!linked) {
                    clearRoomExit(buffer, i, Direction.values()[j]);
                }
            }
        }
    }

    private void linkRooms(final Room room1, final Room room2, final Direction direction) {
        int row1, row2;
        int col1, col2;

        if (direction == Direction.NORTH) {
            row1 = 0;
            row2 = room2.getHeight() - 1;
        } else if (direction == Direction.SOUTH) {
            row1 = room1.getHeight() - 1;
            row2 = 0;
        } else {
            row1 = room1.getHeight() / 2;
            row2 = room2.getHeight() / 2;
        }

        if (direction == Direction.EAST) {
            col1 = room1.getWidth() - 1;
            col2 = 0;
        } else if (direction == Direction.WEST) {
            col1 = 0;
            col2 = room2.getWidth() - 1;
        } else {
            col1 = room1.getWidth() / 2;
            col2 = room2.getWidth() / 2;
        }

        final Color foreground = dungeonMapData.dungeonParameters.getForeground();
        final Color background = dungeonMapData.dungeonParameters.getBackground();
        room1.setExitAbsolute(row1, col1, direction, TileFactory.makeRoomExit(foreground, background, direction));
        room2.setExitAbsolute(row2, col2, direction.opposite(),
                TileFactory.makeRoomExit(foreground, background, direction.opposite()));

        room1.setNeighbor(room2, direction);
        room2.setNeighbor(room1, direction.opposite());
    }

    private static void setRoomType(final byte[] buffer, final int roomNumber, final RoomType roomType) {
        buffer[4 * roomNumber + 0] = (byte) roomType.ordinal();
    }

    private static void setRoomHeight(final byte[] buffer, final int roomNumber, final int height) {
        buffer[4 * roomNumber + 1] = (byte) height;
    }

    private static void setRoomWidth(final byte[] buffer, final int roomNumber, final int width) {
        buffer[4 * roomNumber + 2] = (byte) width;
    }

    private static void setRoomExit(final byte[] buffer, final int roomNumber, final Direction direction) {
        buffer[4 * roomNumber + 3] |= (1 << direction.ordinal());
    }

    private static void clearRoomExit(final byte[] buffer, final int roomNumber, final Direction direction) {
        buffer[4 * roomNumber + 3] &= ~(1 << direction.ordinal());
    }

    private static RoomType getRoomType(final byte[] buffer, final int roomNumber) {
        return RoomType.values()[buffer[4 * roomNumber + 0]];
    }

    private static int getRoomHeight(final byte[] buffer, final int roomNumber) {
        return buffer[4 * roomNumber + 1];
    }

    private static int getRoomWidth(final byte[] buffer, final int roomNumber) {
        return buffer[4 * roomNumber + 2];
    }

    private static byte getRoomExits(final byte[] buffer, final int roomNumber) {
        return buffer[4 * roomNumber + 3];
    }

    private static int getNumRoomExits(final byte[] buffer, final int roomNumber) {
        return Integer.bitCount(getRoomExits(buffer, roomNumber));
    }

    private static boolean getRoomExit(final byte[] buffer, final int roomNumber, final Direction direction) {
        return (getRoomExits(buffer, roomNumber) & (1 << direction.ordinal())) != 0;
    }
}