Java tutorial
/** * 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; } }