Merge pull request #155 from SenseiKiwi/mazes

Mazes
This commit is contained in:
StevenRS11
2014-04-15 07:46:00 -04:00
15 changed files with 1241 additions and 187 deletions

View File

@@ -165,8 +165,8 @@ public class DirectedGraph<U, V>
{ {
Edge<U, V> innerEdge = (Edge<U, V>) edge; Edge<U, V> innerEdge = (Edge<U, V>) edge;
// Check that this node actually belongs to this graph instance. // Check that this edge actually belongs to this graph instance.
// Accepting foreign nodes could corrupt the graph's internal state. // Accepting foreign edges could corrupt the graph's internal state.
if (innerEdge.graphEntry.owner() != edges) if (innerEdge.graphEntry.owner() != edges)
{ {
throw new IllegalArgumentException("The specified edge does not belong to this graph."); throw new IllegalArgumentException("The specified edge does not belong to this graph.");

View File

@@ -0,0 +1,120 @@
package StevenDimDoors.experimental;
import StevenDimDoors.mod_pocketDim.Point3D;
public class LinkPlan
{
private RoomData source;
private RoomData destination;
private Point3D sourcePoint;
private Point3D destinationPoint;
private final boolean entrance;
private final boolean internal;
private LinkPlan(RoomData source, boolean entrance, boolean internal)
{
if (source == null)
{
throw new IllegalArgumentException("source cannot be null.");
}
this.source = source;
this.destination = null;
this.sourcePoint = null;
this.destinationPoint = null;
this.entrance = entrance;
this.internal = internal;
}
public static LinkPlan createInternalLink(RoomData source)
{
LinkPlan plan = new LinkPlan(source, false, true);
source.getOutboundLinks().add(plan);
return plan;
}
public static LinkPlan createEntranceLink(RoomData source)
{
LinkPlan plan = new LinkPlan(source, true, false);
source.getOutboundLinks().add(plan);
return plan;
}
public static LinkPlan createDungeonLink(RoomData source)
{
LinkPlan plan = new LinkPlan(source, false, false);
source.getOutboundLinks().add(plan);
return plan;
}
public RoomData source()
{
return this.source;
}
public RoomData destination()
{
return this.destination;
}
public boolean isEntrance()
{
return entrance;
}
public boolean isInternal()
{
return internal;
}
public void remove()
{
if (source != null)
{
source.getOutboundLinks().remove(this);
source = null;
}
if (destination != null)
{
destination.getInboundLinks().remove(this);
destination = null;
}
}
public void setDestination(RoomData destination)
{
if (!internal)
{
throw new IllegalStateException("LinkPlan.setDestination() is only applicable to internal links.");
}
if (this.destination != null)
{
throw new IllegalStateException("destination can only be set once.");
}
if (destination == null)
{
throw new IllegalArgumentException("destination cannot be null.");
}
this.destination = destination;
destination.getInboundLinks().add(this);
}
public Point3D sourcePoint()
{
return this.sourcePoint;
}
public Point3D destinationPoint()
{
return this.destinationPoint;
}
public void setSourcePoint(Point3D value)
{
this.sourcePoint = value;
}
public void setDestinationPoint(Point3D value)
{
this.destinationPoint = value;
}
}

View File

@@ -1,63 +1,215 @@
package StevenDimDoors.experimental; package StevenDimDoors.experimental;
import java.util.ArrayList;
import java.util.Random; import java.util.Random;
import java.util.Stack;
import net.minecraft.block.Block; import net.minecraft.block.Block;
import net.minecraft.world.World; import net.minecraft.world.World;
import net.minecraft.world.chunk.Chunk; import net.minecraft.world.chunk.Chunk;
import net.minecraft.world.chunk.storage.ExtendedBlockStorage; import net.minecraft.world.chunk.storage.ExtendedBlockStorage;
import StevenDimDoors.experimental.decorators.BaseDecorator;
import StevenDimDoors.experimental.decorators.DecoratorFinder;
import StevenDimDoors.mod_pocketDim.Point3D; import StevenDimDoors.mod_pocketDim.Point3D;
import StevenDimDoors.mod_pocketDim.config.DDProperties;
import StevenDimDoors.mod_pocketDim.core.DimLink;
import StevenDimDoors.mod_pocketDim.core.LinkTypes;
import StevenDimDoors.mod_pocketDim.core.NewDimData;
import StevenDimDoors.mod_pocketDim.core.PocketManager;
public class MazeBuilder public class MazeBuilder
{ {
private static final int POCKET_WALL_GAP = 4;
private static final int DECORATION_CHANCE = 1;
private static final int MAX_DECORATION_CHANCE = 3;
private MazeBuilder() { } private MazeBuilder() { }
public static void generate(World world, int x, int y, int z, Random random) public static void generate(World world, int x, int y, int z, Random random, DDProperties properties)
{ {
// ISSUE FOR LATER: The room needs to be shifted so as to be centered on its entrance
MazeDesign design = MazeDesigner.generate(random); MazeDesign design = MazeDesigner.generate(random);
Point3D offset = new Point3D(x - design.width() / 2, y - design.height() - 1, z - design.length() / 2); Point3D offset = new Point3D(x - design.width() / 2, y - design.height() - 1, z - design.length() / 2);
SphereDecayOperation decay = new SphereDecayOperation(random, 0, 0, Block.stoneBrick.blockID, 2); SphereDecayOperation decay = new SphereDecayOperation(random, 0, 0, Block.stoneBrick.blockID, 2);
buildRooms(design.getRoomGraph(), world, offset); buildRooms(design.getLayout(), world, offset);
carveDoorways(design.getRoomGraph(), world, offset, decay, random); carveDoorways(design.getLayout(), world, offset, decay, random);
//placeDoors(design, world, offset);
applyRandomDestruction(design, world, offset, decay, random); applyRandomDestruction(design, world, offset, decay, random);
decorateRooms(design.getLayout(), world, offset, random, properties);
buildPocketWalls(design, world, offset, properties);
} }
private static void applyRandomDestruction(MazeDesign design, World world, private static void applyRandomDestruction(MazeDesign design, World world,
Point3D offset, SphereDecayOperation decay, Random random) Point3D offset, SphereDecayOperation decay, Random random)
{ {
//final int DECAY_BOX_SIZE = 8 final int DECAY_BOX_SIZE = 7;
final int DECAY_OPERATIONS = 5 + random.nextInt(5);
final int DECAY_ATTEMPTS = 20;
int x, y, z;
int successes = 0;
int attempts = 0;
PartitionNode root = design.getRootPartition();
for (; successes < DECAY_OPERATIONS && attempts < DECAY_ATTEMPTS; attempts++)
{
// Select the coordinates at which to apply the decay operation
x = random.nextInt(design.width()) - DECAY_BOX_SIZE / 2;
y = random.nextInt(design.height()) - DECAY_BOX_SIZE / 2;
z = random.nextInt(design.length()) - DECAY_BOX_SIZE / 2;
// Check that the decay operation would not impact any protected areas
// and mark the affected areas as decayed
if (markDecayArea(x, y, z, DECAY_BOX_SIZE, root))
{
// Apply decay
decay.apply(world, offset.getX() + x, offset.getY() + y, offset.getZ() + z,
DECAY_BOX_SIZE, DECAY_BOX_SIZE, DECAY_BOX_SIZE);
successes++;
}
}
}
private static boolean markDecayArea(int x, int y, int z, int DECAY_BOX_SIZE, PartitionNode<RoomData> root)
{
// Check if a given PartitionNode intersects the decay area. If it's a leaf, then check
// if it's protected or not. Otherwise, check its children. The specific area is valid
// if and only if there are no protected rooms and at least one (unprotected) room in it.
// Also list the unprotected rooms to mark them if the decay operation will proceed.
RoomData room;
PartitionNode<RoomData> partition;
ArrayList<RoomData> targets = new ArrayList<RoomData>();
Stack<PartitionNode<RoomData>> nodes = new Stack<PartitionNode<RoomData>>();
BoundingBox decayBounds = new BoundingBox(x, y, z, DECAY_BOX_SIZE, DECAY_BOX_SIZE, DECAY_BOX_SIZE);
// Use depth-first search to explore all intersecting partitions
nodes.push(root);
while (!nodes.isEmpty())
{
partition = nodes.pop();
if (decayBounds.intersects(partition))
{
if (partition.isLeaf())
{
room = partition.getData();
if (room.isProtected())
return false;
targets.add(room);
}
else
{
if (partition.leftChild() != null)
nodes.push(partition.leftChild());
if (partition.rightChild() != null)
nodes.push(partition.rightChild());
}
}
}
// If execution has reached this point, then there were no protected rooms.
// Mark all intersecting rooms as decayed.
for (RoomData target : targets)
{
target.setDecayed(true);
}
return !targets.isEmpty();
} }
private static void buildRooms(DirectedGraph<PartitionNode, DoorwayData> roomGraph, World world, Point3D offset) private static void buildRooms(DirectedGraph<RoomData, DoorwayData> layout, World world, Point3D offset)
{ {
for (IGraphNode<PartitionNode, DoorwayData> node : roomGraph.nodes()) for (IGraphNode<RoomData, DoorwayData> node : layout.nodes())
{ {
PartitionNode room = node.data(); PartitionNode room = node.data().getPartitionNode();
buildBox(world, offset, room.minCorner(), room.maxCorner(), Block.stoneBrick.blockID, 0); buildBox(world, offset, room.minCorner(), room.maxCorner(), Block.stoneBrick.blockID, 0);
} }
} }
private static void carveDoorways(DirectedGraph<PartitionNode, DoorwayData> roomGraph, World world, private static void decorateRooms(DirectedGraph<RoomData, DoorwayData> layout,
World world, Point3D offset, Random random, DDProperties properties)
{
RoomData room;
BaseDecorator decorator;
PartitionNode<RoomData> partition;
ArrayList<LinkPlan> links = new ArrayList<LinkPlan>();
// Iterate over all rooms and apply decorators
for (IGraphNode<RoomData, DoorwayData> node : layout.nodes())
{
room = node.data();
partition = room.getPartitionNode();
links.addAll(room.getOutboundLinks());
// Protected rooms must be decorated because they have links.
// Otherwise, choose randomly whether to decorate.
if (room.isProtected() || random.nextInt(MAX_DECORATION_CHANCE) < DECORATION_CHANCE)
{
decorator = DecoratorFinder.find(room, random);
if (decorator != null)
{
decorator.decorate(room, world, offset, random, properties);
}
}
}
// Iterate over all link plans and place links in the world
NewDimData dimension = PocketManager.getDimensionData(world);
for (LinkPlan plan : links)
{
createLinkFromPlan(plan, dimension, world);
}
}
private static void createLinkFromPlan(LinkPlan plan, NewDimData dimension, World world)
{
// TODO: Support entrances! Right now we'll treat them as dungeon doors for testing
DimLink link;
Point3D source;
Point3D destination;
int orientation;
source = plan.sourcePoint();
orientation = world.getBlockMetadata(source.getX(), source.getY(), source.getZ()) & 3;
// Check the link type and set the destination accordingly
if (plan.isInternal())
{
// Create a link between sections
destination = plan.destinationPoint();
link = dimension.createLink(source.getX(), source.getY(), source.getZ(), LinkTypes.DUNGEON, orientation);
dimension.setDestination(link, destination.getX(), destination.getY(), destination.getZ());
}
else
{
// Create a dungeon link
dimension.createLink(source.getX(), source.getY(), source.getZ(), LinkTypes.DUNGEON, orientation);
}
}
private static void carveDoorways(DirectedGraph<RoomData, DoorwayData> layout, World world,
Point3D offset, SphereDecayOperation decay, Random random) Point3D offset, SphereDecayOperation decay, Random random)
{ {
char axis; char axis;
Point3D lower; Point3D lower;
DoorwayData doorway; DoorwayData doorway;
for (IGraphNode<PartitionNode, DoorwayData> node : roomGraph.nodes()) for (IGraphNode<RoomData, DoorwayData> node : layout.nodes())
{ {
for (IEdge<PartitionNode, DoorwayData> passage : node.outbound()) for (IEdge<RoomData, DoorwayData> passage : node.outbound())
{ {
// Carve out the passage
doorway = passage.data(); doorway = passage.data();
axis = doorway.axis(); axis = doorway.axis();
lower = doorway.minCorner(); lower = doorway.minCorner();
carveDoorway(world, axis, offset.getX() + lower.getX(), offset.getY() + lower.getY(), carveDoorway(world, axis, offset.getX() + lower.getX(), offset.getY() + lower.getY(),
offset.getZ() + lower.getZ(), doorway.width(), doorway.height(), doorway.length(), offset.getZ() + lower.getZ(), doorway.width(), doorway.height(), doorway.length(),
decay, random); decay, random);
// If this is a vertical passage, then mark the upper room as decayed
if (axis == DoorwayData.Y_AXIS)
{
passage.tail().data().setDecayed(true);
}
} }
} }
} }
@@ -68,6 +220,8 @@ public class MazeBuilder
final int MIN_DOUBLE_DOOR_SPAN = 10; final int MIN_DOUBLE_DOOR_SPAN = 10;
int gap; int gap;
int rx;
int rz;
switch (axis) switch (axis)
{ {
case DoorwayData.X_AXIS: case DoorwayData.X_AXIS:
@@ -132,9 +286,10 @@ public class MazeBuilder
{ {
gap = 6; gap = 6;
} }
decay.apply(world, rx = x + random.nextInt(width - gap - 1) + 1;
x + random.nextInt(width - gap - 1) + 1, y - 1, rz = z + random.nextInt(length - gap - 1) + 1;
z + random.nextInt(length - gap - 1) + 1, gap, 4, gap); carveHole(world, rx + gap / 2, y, rz + gap / 2);
decay.apply(world, rx, y - 1, rz, gap, 4, gap);
} }
else else
{ {
@@ -165,7 +320,19 @@ public class MazeBuilder
setBlockDirectly(world, x, y, z, 0, 0); setBlockDirectly(world, x, y, z, 0, 0);
setBlockDirectly(world, x, y + 1, z, 0, 0); setBlockDirectly(world, x, y + 1, z, 0, 0);
} }
private static void buildPocketWalls(MazeDesign design, World world, Point3D offset, DDProperties properties)
{
// Build the inner Fabric of Reality box
Point3D minCorner = new Point3D(-POCKET_WALL_GAP - 1, -POCKET_WALL_GAP - 1, -POCKET_WALL_GAP - 1);
Point3D maxCorner = new Point3D(design.width() + POCKET_WALL_GAP, design.height() + POCKET_WALL_GAP, design.length() + POCKET_WALL_GAP);
buildBox(world, offset, minCorner, maxCorner, properties.FabricBlockID, 0);
// Build the outer Eternal Fabric box
minCorner.add(-1, -1, -1);
maxCorner.add(1, 1, 1);
buildBox(world, offset, minCorner, maxCorner, properties.PermaFabricBlockID, 0);
}
private static void buildBox(World world, Point3D offset, Point3D minCorner, Point3D maxCorner, int blockID, int metadata) private static void buildBox(World world, Point3D offset, Point3D minCorner, Point3D maxCorner, int blockID, int metadata)
{ {
@@ -189,15 +356,15 @@ public class MazeBuilder
} }
for (x = minX; x <= maxX; x++) for (x = minX; x <= maxX; x++)
{ {
for (y = minY; y <= maxY; y++) for (y = minY + 1; y < maxY; y++)
{ {
setBlockDirectly(world, x, y, minZ, blockID, metadata); setBlockDirectly(world, x, y, minZ, blockID, metadata);
setBlockDirectly(world, x, y, maxZ, blockID, metadata); setBlockDirectly(world, x, y, maxZ, blockID, metadata);
} }
} }
for (z = minZ; z <= maxZ; z++) for (z = minZ + 1; z < maxZ; z++)
{ {
for (y = minY; y <= maxY; y++) for (y = minY + 1; y < maxY; y++)
{ {
setBlockDirectly(world, minX, y, z, blockID, metadata); setBlockDirectly(world, minX, y, z, blockID, metadata);
setBlockDirectly(world, maxX, y, z, blockID, metadata); setBlockDirectly(world, maxX, y, z, blockID, metadata);

View File

@@ -4,37 +4,23 @@ import java.util.ArrayList;
public class MazeDesign public class MazeDesign
{ {
private PartitionNode root; private PartitionNode<RoomData> root;
private DirectedGraph<PartitionNode, DoorwayData> rooms; private DirectedGraph<RoomData, DoorwayData> layout;
private ArrayList<IGraphNode<PartitionNode, DoorwayData>> cores;
private ArrayList<BoundingBox> protectedAreas;
public MazeDesign(PartitionNode root, DirectedGraph<PartitionNode, DoorwayData> rooms, public MazeDesign(PartitionNode<RoomData> root, DirectedGraph<RoomData, DoorwayData> layout)
ArrayList<IGraphNode<PartitionNode, DoorwayData>> cores)
{ {
this.root = root; this.root = root;
this.rooms = rooms; this.layout = layout;
this.cores = cores;
} }
public PartitionNode getRootPartition() public PartitionNode<RoomData> getRootPartition()
{ {
return root; return root;
} }
public DirectedGraph<PartitionNode, DoorwayData> getRoomGraph() public DirectedGraph<RoomData, DoorwayData> getLayout()
{ {
return rooms; return layout;
}
public ArrayList<IGraphNode<PartitionNode, DoorwayData>> getCoreNodes()
{
return cores;
}
public ArrayList<BoundingBox> getProtectedAreas()
{
return protectedAreas;
} }
public int width() public int width()

View File

@@ -1,8 +1,9 @@
package StevenDimDoors.experimental; package StevenDimDoors.experimental;
import java.lang.reflect.Array;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap; import java.util.HashSet;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.Queue; import java.util.Queue;
import java.util.Random; import java.util.Random;
@@ -25,54 +26,46 @@ public class MazeDesigner
public static MazeDesign generate(Random random) public static MazeDesign generate(Random random)
{ {
// Construct a random binary space partitioning of our maze volume // Construct a random binary space partitioning of our maze volume
PartitionNode root = partitionRooms(MAZE_WIDTH, MAZE_HEIGHT, MAZE_LENGTH, SPLIT_COUNT, random); PartitionNode<RoomData> root = partitionRooms(MAZE_WIDTH, MAZE_HEIGHT, MAZE_LENGTH, SPLIT_COUNT, random);
// List all the leaf nodes of the partition tree, which denote individual rooms // Attach rooms to all the leaf nodes of the partition tree
ArrayList<PartitionNode> partitions = new ArrayList<PartitionNode>(1 << SPLIT_COUNT); ArrayList<RoomData> rooms = new ArrayList<RoomData>(1 << SPLIT_COUNT);
listRoomPartitions(root, partitions); attachRooms(root, rooms);
// Shuffle the list of rooms so that they're not listed in any ordered way in the room graph
// This is the only convenient way of randomizing the maze sections generated later
Collections.shuffle(rooms, random);
// Construct an adjacency graph of the rooms we've carved out. Two rooms are // Construct an adjacency graph of the rooms we've carved out. Two rooms are
// considered adjacent if and only if a doorway could connect them. Their // considered adjacent if and only if a doorway could connect them. Their
// common boundary must be large enough for a doorway. // common boundary must be large enough for a doorway.
DirectedGraph<PartitionNode, DoorwayData> rooms = createRoomGraph(root, partitions, random); DirectedGraph<RoomData, DoorwayData> layout = createRoomGraph(root, rooms, random);
// Cut out random subgraphs from the adjacency graph // Cut out random subgraphs from the adjacency graph
ArrayList<IGraphNode<PartitionNode, DoorwayData>> cores = createMazeSections(rooms, random); ArrayList<RoomData> cores = createMazeSections(layout, random);
// Remove unnecessary passages through floors/ceilings and some from the walls // Remove unnecessary passages through floors/ceilings and some from the walls
for (IGraphNode<PartitionNode, DoorwayData> core : cores) for (RoomData core : cores)
{ {
pruneDoorways(core, rooms, random); pruneDoorways(core.getLayoutNode(), layout, random);
} }
return new MazeDesign(root, rooms, cores); // Set up the placement of dimensional doors within the maze
createMazeLinks(layout, cores, random);
return new MazeDesign(root, layout);
} }
private static void listRoomPartitions(PartitionNode node, ArrayList<PartitionNode> partitions) private static void attachRooms(PartitionNode<RoomData> node, ArrayList<RoomData> partitions)
{ {
if (node.isLeaf()) if (node.isLeaf())
{ {
partitions.add(node); partitions.add(new RoomData(node));
} }
else else
{ {
listRoomPartitions(node.leftChild(), partitions); attachRooms(node.leftChild(), partitions);
listRoomPartitions(node.rightChild(), partitions); attachRooms(node.rightChild(), partitions);
}
}
private static void removeRoomPartitions(PartitionNode node)
{
// Remove a node and any of its ancestors that become leaf nodes
PartitionNode parent;
PartitionNode current;
current = node;
while (current != null && current.isLeaf())
{
parent = current.parent();
current.remove();
current = parent;
} }
} }
@@ -140,35 +133,25 @@ public class MazeDesigner
} }
} }
private static DirectedGraph<PartitionNode, DoorwayData> createRoomGraph(PartitionNode root, ArrayList<PartitionNode> partitions, Random random) private static DirectedGraph<RoomData, DoorwayData> createRoomGraph(PartitionNode<RoomData> root, ArrayList<RoomData> rooms, Random random)
{ {
DirectedGraph<PartitionNode, DoorwayData> roomGraph = new DirectedGraph<PartitionNode, DoorwayData>(); DirectedGraph<RoomData, DoorwayData> layout = new DirectedGraph<RoomData, DoorwayData>();
HashMap<PartitionNode, IGraphNode<PartitionNode, DoorwayData>> roomsToGraph = new HashMap<PartitionNode, IGraphNode<PartitionNode, DoorwayData>>(2 * partitions.size());
// Shuffle the list of rooms so that they're not listed in any ordered way in the room graph
// This is the only convenient way of randomizing the maze sections generated later
Collections.shuffle(partitions, random);
// Add all rooms to a graph // Add all rooms to a graph
// Also add them to a map so we can associate rooms with their graph nodes for (RoomData room : rooms)
// The map is needed for linking graph nodes based on adjacent partitions
for (PartitionNode partition : partitions)
{ {
roomsToGraph.put(partition, roomGraph.addNode(partition)); room.addToLayout(layout);
} }
// Add edges for each room // Add edges for each room
for (IGraphNode<PartitionNode, DoorwayData> node : roomGraph.nodes()) for (IGraphNode<RoomData, DoorwayData> node : layout.nodes())
{ {
findDoorways(node, root, roomsToGraph, roomGraph); findDoorways(node.data(), root, layout);
} }
return layout;
return roomGraph;
} }
private static void findDoorways(IGraphNode<PartitionNode, DoorwayData> roomNode, PartitionNode root, private static void findDoorways(RoomData room, PartitionNode<RoomData> root,
HashMap<PartitionNode, IGraphNode<PartitionNode, DoorwayData>> roomsToGraph, DirectedGraph<RoomData, DoorwayData> layout)
DirectedGraph<PartitionNode, DoorwayData> roomGraph)
{ {
// This function finds rooms adjacent to a specified room that could be connected // This function finds rooms adjacent to a specified room that could be connected
// to it through a doorway. Edges are added to the room graph to denote rooms that // to it through a doorway. Edges are added to the room graph to denote rooms that
@@ -186,7 +169,7 @@ public class MazeDesigner
// there will always be a way to walk from any room to any other room. // there will always be a way to walk from any room to any other room.
boolean[][] detected; boolean[][] detected;
PartitionNode adjacent; PartitionNode<RoomData> adjacent;
int a, b, c; int a, b, c;
int p, q, r; int p, q, r;
@@ -195,11 +178,10 @@ public class MazeDesigner
Point3D otherMin; Point3D otherMin;
Point3D otherMax; Point3D otherMax;
DoorwayData doorway; DoorwayData doorway;
IGraphNode<PartitionNode, DoorwayData> adjacentNode;
PartitionNode room = roomNode.data(); PartitionNode partition = room.getPartitionNode();
Point3D minCorner = room.minCorner(); Point3D minCorner = partition.minCorner();
Point3D maxCorner = room.maxCorner(); Point3D maxCorner = partition.maxCorner();
int minX = minCorner.getX(); int minX = minCorner.getX();
int minY = minCorner.getY(); int minY = minCorner.getY();
@@ -209,9 +191,9 @@ public class MazeDesigner
int maxY = maxCorner.getY(); int maxY = maxCorner.getY();
int maxZ = maxCorner.getZ(); int maxZ = maxCorner.getZ();
int width = room.width(); int width = partition.width();
int height = room.height(); int height = partition.height();
int length = room.length(); int length = partition.length();
if (maxZ < root.maxCorner().getZ()) if (maxZ < root.maxCorner().getZ())
{ {
@@ -247,8 +229,7 @@ public class MazeDesigner
otherMin = new Point3D(minXI, minYI, maxZ); otherMin = new Point3D(minXI, minYI, maxZ);
otherMax = new Point3D(maxXI, maxYI, maxZ + 1); otherMax = new Point3D(maxXI, maxYI, maxZ + 1);
doorway = new DoorwayData(otherMin, otherMax, DoorwayData.Z_AXIS); doorway = new DoorwayData(otherMin, otherMax, DoorwayData.Z_AXIS);
adjacentNode = roomsToGraph.get(adjacent); layout.addEdge(room.getLayoutNode(), adjacent.getData().getLayoutNode(), doorway);
roomGraph.addEdge(roomNode, adjacentNode, doorway);
} }
} }
else else
@@ -295,8 +276,7 @@ public class MazeDesigner
otherMin = new Point3D(maxX, minYI, minZI); otherMin = new Point3D(maxX, minYI, minZI);
otherMax = new Point3D(maxX + 1, maxYI, maxZI); otherMax = new Point3D(maxX + 1, maxYI, maxZI);
doorway = new DoorwayData(otherMin, otherMax, DoorwayData.X_AXIS); doorway = new DoorwayData(otherMin, otherMax, DoorwayData.X_AXIS);
adjacentNode = roomsToGraph.get(adjacent); layout.addEdge(room.getLayoutNode(), adjacent.getData().getLayoutNode(), doorway);
roomGraph.addEdge(roomNode, adjacentNode, doorway);
} }
} }
else else
@@ -343,8 +323,7 @@ public class MazeDesigner
otherMin = new Point3D(minXI, maxY, minZI); otherMin = new Point3D(minXI, maxY, minZI);
otherMax = new Point3D(maxXI, maxY + 1, maxZI); otherMax = new Point3D(maxXI, maxY + 1, maxZI);
doorway = new DoorwayData(otherMin, otherMax, DoorwayData.Y_AXIS); doorway = new DoorwayData(otherMin, otherMax, DoorwayData.Y_AXIS);
adjacentNode = roomsToGraph.get(adjacent); layout.addEdge(room.getLayoutNode(), adjacent.getData().getLayoutNode(), doorway);
roomGraph.addEdge(roomNode, adjacentNode, doorway);
} }
} }
else else
@@ -359,108 +338,130 @@ public class MazeDesigner
//Done! //Done!
} }
private static ArrayList<IGraphNode<PartitionNode, DoorwayData>> createMazeSections(DirectedGraph<PartitionNode, DoorwayData> roomGraph, Random random) private static ArrayList<RoomData> createMazeSections(DirectedGraph<RoomData, DoorwayData> layout, Random random)
{ {
// The randomness of the sections generated here hinges on // The randomness of the sections generated here hinges on
// the nodes in the graph being in a random order. We assume // the nodes in the graph being in a random order. We assume
// that was handled in a previous step! // that was handled in a previous step!
final int MAX_DISTANCE = 2; // We split the maze into sections by choosing core rooms and removing
// rooms that are a certain number of doorways away. However, for a section
// to be valid, it must also have enough space for at least two doors in
// rooms without floor holes. If a section can't fit two doors, more
// neighboring rooms are added until the necessary space is found or the
// search space is exhausted.
final int MAX_DISTANCE = 2 + random.nextInt(2);
final int MIN_SECTION_ROOMS = 5; final int MIN_SECTION_ROOMS = 5;
final int MIN_SECTION_CAPACITY = 2;
int distance; int distance;
IGraphNode<PartitionNode, DoorwayData> current; int capacity;
IGraphNode<PartitionNode, DoorwayData> neighbor; RoomData room;
RoomData neighbor;
boolean hasHoles;
IGraphNode<RoomData, DoorwayData> roomNode;
ArrayList<IGraphNode<PartitionNode, DoorwayData>> cores = new ArrayList<IGraphNode<PartitionNode, DoorwayData>>(); ArrayList<RoomData> cores = new ArrayList<RoomData>();
ArrayList<IGraphNode<PartitionNode, DoorwayData>> removals = new ArrayList<IGraphNode<PartitionNode, DoorwayData>>(); ArrayList<RoomData> section = new ArrayList<RoomData>();
ArrayList<IGraphNode<PartitionNode, DoorwayData>> section = new ArrayList<IGraphNode<PartitionNode, DoorwayData>>(); ArrayList<IGraphNode<RoomData, DoorwayData>> nodes = new ArrayList<IGraphNode<RoomData, DoorwayData>>(layout.nodeCount());
Queue<IGraphNode<PartitionNode, DoorwayData>> ordering = new LinkedList<IGraphNode<PartitionNode, DoorwayData>>(); Queue<RoomData> ordering = new LinkedList<RoomData>();
HashMap<IGraphNode<PartitionNode, DoorwayData>, Integer> distances = new HashMap<IGraphNode<PartitionNode, DoorwayData>, Integer>();
// List all graph nodes so that we can iterate over this list instead
// of using the graph's iterator. That avoids the risk of breaking
// the graph's iterator during removals.
for (IGraphNode<RoomData, DoorwayData> node : layout.nodes())
{
nodes.add(node);
}
// Repeatedly generate sections until all nodes have been visited // Repeatedly generate sections until all nodes have been visited
for (IGraphNode<PartitionNode, DoorwayData> node : roomGraph.nodes()) for (IGraphNode<RoomData, DoorwayData> node : nodes)
{ {
// If this node hasn't been visited, then use it as the core of a new section // If this room hasn't been visited (distance = -1), then use it as the core of a new section
// Otherwise, ignore it, since it was already processed // Otherwise, ignore it, since it was already processed. Also make sure to check that room
if (!distances.containsKey(node)) // isn't null, which happens if the room was removed previously.
room = node.data();
if (room != null && room.getDistance() < 0)
{ {
// Perform a breadth-first search to tag surrounding nodes with distances // Perform a breadth-first search to tag surrounding nodes with distances
distances.put(node, 0); ordering.add(room);
ordering.add(node); room.setDistance(0);
section.clear(); section.clear();
capacity = 0;
while (!ordering.isEmpty()) while (room != null && (room.getDistance() <= MAX_DISTANCE || capacity < 2))
{ {
current = ordering.remove(); ordering.remove();
distance = distances.get(current) + 1; section.add(room);
roomNode = room.getLayoutNode();
distance = room.getDistance() + 1;
hasHoles = false;
if (distance <= MAX_DISTANCE + 1) // Visit neighboring rooms and assign them distances,
// if they don't have a proper distance assigned already.
// Also check for floor holes.
for (IEdge<RoomData, DoorwayData> edge : roomNode.inbound())
{ {
section.add(current); neighbor = edge.head().data();
if (neighbor.getDistance() < 0)
// Visit neighboring nodes and assign them distances, if they don't
// have a distance assigned already
for (IEdge<PartitionNode, DoorwayData> edge : current.inbound())
{ {
neighbor = edge.head(); neighbor.setDistance(distance);
if (!distances.containsKey(neighbor)) ordering.add(neighbor);
{
distances.put(neighbor, distance);
ordering.add(neighbor);
}
} }
for (IEdge<PartitionNode, DoorwayData> edge : current.outbound()) if (edge.data().axis() == DoorwayData.Y_AXIS)
{ {
neighbor = edge.tail(); hasHoles = true;
if (!distances.containsKey(neighbor))
{
distances.put(neighbor, distance);
ordering.add(neighbor);
}
} }
} }
else for (IEdge<RoomData, DoorwayData> edge : roomNode.outbound())
{ {
removals.add(current); neighbor = edge.tail().data();
break; if (neighbor.getDistance() < 0)
{
neighbor.setDistance(distance);
ordering.add(neighbor);
}
} }
// Count this room's door capacity if it has no floor holes
if (!hasHoles)
{
capacity += room.estimateDoorCapacity();
}
room = ordering.peek();
} }
// List nodes that have a distance of exactly MAX_DISTANCE + 1 // The remaining rooms in the ordering are those that are at the
// Those are precisely the nodes that remain in the queue // frontier of structure. They must be removed to create a gap
// We can't remove them immediately because that could break // between this section and other sections.
// the iterator for the graph.
while (!ordering.isEmpty()) while (!ordering.isEmpty())
{ {
removals.add(ordering.remove()); ordering.remove().remove();
} }
// Check if this section contains enough rooms // Check if this section contains enough rooms and capacity for doors
if (section.size() >= MIN_SECTION_ROOMS) if (section.size() >= MIN_SECTION_ROOMS && capacity >= MIN_SECTION_CAPACITY)
{ {
cores.add(node); cores.add(node.data());
} }
else else
{ {
removals.addAll(section); // Discard the whole section
for (RoomData target : section)
{
target.remove();
}
} }
} }
} }
// Remove all the nodes that were listed for removal
// Also remove unused partitions from the partition tree
for (IGraphNode<PartitionNode, DoorwayData> node : removals)
{
removeRoomPartitions(node.data());
roomGraph.removeNode(node);
}
return cores; return cores;
} }
private static void pruneDoorways(IGraphNode<PartitionNode, DoorwayData> core, private static void pruneDoorways(IGraphNode<RoomData, DoorwayData> core,
DirectedGraph<PartitionNode, DoorwayData> rooms, Random random) DirectedGraph<RoomData, DoorwayData> layout, Random random)
{ {
// We receive a node for one of the rooms in a section of the maze // We receive a node for one of the rooms in a section of the maze
// and we need to remove as many floor doorways as possible while // and we need to remove as many floor doorways as possible while
@@ -478,12 +479,12 @@ public class MazeDesigner
// idea applies for the other doorways, plus some randomness. // idea applies for the other doorways, plus some randomness.
// First, list all nodes in the subgraph // First, list all nodes in the subgraph
IGraphNode<PartitionNode, DoorwayData> current; IGraphNode<RoomData, DoorwayData> current;
IGraphNode<PartitionNode, DoorwayData> neighbor; IGraphNode<RoomData, DoorwayData> neighbor;
Stack<IGraphNode<PartitionNode, DoorwayData>> ordering = new Stack<IGraphNode<PartitionNode, DoorwayData>>(); Stack<IGraphNode<RoomData, DoorwayData>> ordering = new Stack<IGraphNode<RoomData, DoorwayData>>();
ArrayList<IGraphNode<PartitionNode, DoorwayData>> subgraph = new ArrayList<IGraphNode<PartitionNode, DoorwayData>>(64); ArrayList<IGraphNode<RoomData, DoorwayData>> subgraph = new ArrayList<IGraphNode<RoomData, DoorwayData>>(64);
DisjointSet<IGraphNode<PartitionNode, DoorwayData>> components = new DisjointSet<IGraphNode<PartitionNode, DoorwayData>>(128); DisjointSet<IGraphNode<RoomData, DoorwayData>> components = new DisjointSet<IGraphNode<RoomData, DoorwayData>>(128);
ordering.add(core); ordering.add(core);
components.makeSet(core); components.makeSet(core);
@@ -492,7 +493,7 @@ public class MazeDesigner
current = ordering.pop(); current = ordering.pop();
subgraph.add(current); subgraph.add(current);
for (IEdge<PartitionNode, DoorwayData> edge : current.inbound()) for (IEdge<RoomData, DoorwayData> edge : current.inbound())
{ {
neighbor = edge.head(); neighbor = edge.head();
if (components.makeSet(neighbor)) if (components.makeSet(neighbor))
@@ -500,7 +501,7 @@ public class MazeDesigner
ordering.add(neighbor); ordering.add(neighbor);
} }
} }
for (IEdge<PartitionNode, DoorwayData> edge : current.outbound()) for (IEdge<RoomData, DoorwayData> edge : current.outbound())
{ {
neighbor = edge.tail(); neighbor = edge.tail();
if (components.makeSet(neighbor)) if (components.makeSet(neighbor))
@@ -510,15 +511,17 @@ public class MazeDesigner
} }
} }
// Now iterate over the list of nodes and merge their sets // Now iterate over the list of nodes and merge their sets based on
// We only have to look at outbound edges since inbound edges mirror them // being connected by X_AXIS or Z_AXIS doorways. We only have to look
// Also list any Y_AXIS doorways we come across // at outbound edges since inbound edges mirror them. List any Y_AXIS
ArrayList<IEdge<PartitionNode, DoorwayData>> targets = // doorways we come across to consider removing them later, depending
new ArrayList<IEdge<PartitionNode, DoorwayData>>(); // on their impact on connectedness.
ArrayList<IEdge<RoomData, DoorwayData>> targets =
new ArrayList<IEdge<RoomData, DoorwayData>>();
for (IGraphNode<PartitionNode, DoorwayData> room : subgraph) for (IGraphNode<RoomData, DoorwayData> room : subgraph)
{ {
for (IEdge<PartitionNode, DoorwayData> passage : room.outbound()) for (IEdge<RoomData, DoorwayData> passage : room.outbound())
{ {
if (passage.data().axis() != DoorwayData.Y_AXIS) if (passage.data().axis() != DoorwayData.Y_AXIS)
{ {
@@ -535,11 +538,12 @@ public class MazeDesigner
Collections.shuffle(targets, random); Collections.shuffle(targets, random);
// Merge sets together and remove unnecessary doorways // Merge sets together and remove unnecessary doorways
for (IEdge<PartitionNode, DoorwayData> passage : targets) for (IEdge<RoomData, DoorwayData> passage : targets)
{ {
if (!components.mergeSets(passage.head(), passage.tail())) if (!components.mergeSets(passage.head(), passage.tail()))
{ {
rooms.removeEdge(passage); layout.removeEdge(passage);
} }
} }
@@ -548,13 +552,13 @@ public class MazeDesigner
components.clear(); components.clear();
targets.clear(); targets.clear();
for (IGraphNode<PartitionNode, DoorwayData> room : subgraph) for (IGraphNode<RoomData, DoorwayData> room : subgraph)
{ {
components.makeSet(room); components.makeSet(room);
} }
for (IGraphNode<PartitionNode, DoorwayData> room : subgraph) for (IGraphNode<RoomData, DoorwayData> room : subgraph)
{ {
for (IEdge<PartitionNode, DoorwayData> passage : room.outbound()) for (IEdge<RoomData, DoorwayData> passage : room.outbound())
{ {
if (passage.data().axis() == DoorwayData.Y_AXIS) if (passage.data().axis() == DoorwayData.Y_AXIS)
{ {
@@ -567,13 +571,198 @@ public class MazeDesigner
} }
} }
Collections.shuffle(targets, random); Collections.shuffle(targets, random);
for (IEdge<PartitionNode, DoorwayData> passage : targets) for (IEdge<RoomData, DoorwayData> passage : targets)
{ {
if (!components.mergeSets(passage.head(), passage.tail()) && random.nextBoolean()) if (!components.mergeSets(passage.head(), passage.tail()) && random.nextBoolean())
{ {
rooms.removeEdge(passage); layout.removeEdge(passage);
} }
} }
} }
private static void createMazeLinks(DirectedGraph<RoomData, DoorwayData> layout,
ArrayList<RoomData> cores, Random random)
{
// We have 4 objectives here...
// 1. Place the entrance to the maze
// 2. Place links to other dungeons
// 3. Place internal links connecting the different sections of the maze
// 4. Place more internal links to confuse people
// We need to start by building up data for each section, such as their
// door capacities and the rooms available for placing doors.
int index;
int count;
SectionData selection;
SectionData destination;
ArrayList<SectionData> allSections;
ArrayList<SectionData> usableSections;
// Check if there is only one section. Our concerns differ depending
// on whether there is one or more than one.
if (cores.size() > 1)
{
// More than 1 section
allSections = new ArrayList<SectionData>(cores.size());
for (RoomData core : cores)
{
allSections.add( SectionData.createFromCore(core.getLayoutNode()) );
}
usableSections = (ArrayList<SectionData>) allSections.clone();
// Select the room in which to place the entrance.
// We can safely consider all sections because createMazeSections()
// guarantees that each one has at least the capacity for 2 doors.
// Remove the selected section if it falls below a capacity of 2
// since we need to leave at least 1 capacity for section linking.
index = random.nextInt(usableSections.size());
selection = usableSections.get(index);
selection.createEntranceLink(random);
if (selection.capacity() <= 1)
{
usableSections.remove(index);
}
// Place 3 to 4 dungeon doors in random sections
// Remove any sections that fall under a capacity of 2.
count = 3 + random.nextInt(2);
for (; count > 0 && !usableSections.isEmpty(); count--)
{
index = random.nextInt(usableSections.size());
selection = usableSections.get(index);
selection.createDungeonLink(random);
if (selection.capacity() <= 1)
{
usableSections.remove(index);
}
}
// The next task is to place internal links. These links must connect
// the different maze sections to create a strongly connected graph.
linkMazeSections(allSections, random);
// Add 1 to 3 extra internal links to confuse people
usableSections.clear();
for (SectionData section : allSections)
{
if (section.capacity() > 0)
{
usableSections.add(section);
}
}
count = 1 + random.nextInt(3);
for (; count > 0 && !usableSections.isEmpty(); count--)
{
index = random.nextInt(usableSections.size());
selection = usableSections.get(index);
destination = allSections.get( random.nextInt(allSections.size()) );
selection.reserveSectionLink(destination, random);
if (selection.capacity() == 0)
{
usableSections.remove(index);
}
}
// Finally, make sure to process all reservations for section links.
for (SectionData section : allSections)
{
section.processReservedLinks(random);
}
}
else
{
// Only 1 section
selection = SectionData.createFromCore(cores.get(0).getLayoutNode());
// Place entrance door in a random room
selection.createEntranceLink(random);
// Place 3 to 4 dungeon doors or fewer, based on capacity
count = Math.min(3 + random.nextInt(2), selection.capacity());
for (; count > 0; count--)
{
selection.createDungeonLink(random);
}
}
}
private static void linkMazeSections(ArrayList<SectionData> sections, Random random)
{
// This algorithm constructs links sections together using Dimensional Doors
// to create a random strongly connected graph. It takes into account capacity
// constraints as well. We assume that all sections have at least 1 door capacity.
final int EXTENSION_CHANCE = 2;
final int MAX_EXTENSION_CHANCE = 3;
int index;
SectionData start;
SectionData current;
SectionData next;
// Total spare capacity of the sections not in "remaining"
int capacity;
// Sections not in the graph
ArrayList<SectionData> remaining = (ArrayList<SectionData>) sections.clone();
// Sections that are part of an incomplete cycle
ArrayList<SectionData> attached = new ArrayList<SectionData>(sections.size());
// Sections that are part of a cycle and thus strongly connected
ArrayList<SectionData> connected = new ArrayList<SectionData>(sections.size());
// Sections that are part of a cycle and have spare capacity - used to start new cycles
ArrayList<SectionData> starters = new ArrayList<SectionData>(sections.size());
// Shuffle remaining to achieve randomness
Collections.shuffle(remaining, random);
// Remove the starting node to serve as the base of our strongly connected graph
start = remaining.remove(remaining.size() - 1);
starters.add(start);
connected.add(start);
capacity = start.capacity();
// Repeat until all sections are connected
while (!remaining.isEmpty())
{
// Select a section from which to start a new cycle
index = random.nextInt(starters.size());
start = starters.get(index);
// Select the first new section in the cycle and link to it
current = remaining.remove(remaining.size() - 1);
attached.add(current);
start.reserveSectionLink(current, random);
// Add the current section's capacity to the total, but subtract two to account
// for the link just created and for the future link from this section to another
capacity += current.capacity() - 2;
// Remove the starting section from starters if it has exhausted its capacity
if (start.capacity() == 0)
{
starters.remove(index);
}
// Continue attaching sections to the partial cycle while there are are still sections
// left to be added and we have no spare capacity. Or we could randomly decide to
// continue even with spare capacity. Spare capacity means we could start a new cycle
// safely and still achieve strong connectivity. Randomness here influences the kinds
// of graphs we can get.
while (!remaining.isEmpty() && (capacity == 0 ||
random.nextInt(MAX_EXTENSION_CHANCE) < EXTENSION_CHANCE))
{
next = remaining.remove(remaining.size() - 1);
attached.add(next);
current.reserveSectionLink(next, random);
// Account for this section's capacity, but subtract one for
// the future link that will connect this section to another
capacity += next.capacity() - 1;
current = next;
}
next = connected.get(random.nextInt(connected.size()));
current.reserveSectionLink(next, random);
for (SectionData section : attached)
{
connected.add(section);
if (section.capacity() > 0)
{
starters.add(section);
}
}
}
// Done! At this point, all sections are connected.
}
} }

View File

@@ -2,11 +2,12 @@ package StevenDimDoors.experimental;
import StevenDimDoors.mod_pocketDim.Point3D; import StevenDimDoors.mod_pocketDim.Point3D;
public class PartitionNode extends BoundingBox public class PartitionNode<T> extends BoundingBox
{ {
private PartitionNode parent; private PartitionNode parent;
private PartitionNode leftChild = null; private PartitionNode leftChild = null;
private PartitionNode rightChild = null; private PartitionNode rightChild = null;
private T data = null;
public PartitionNode(int width, int height, int length) public PartitionNode(int width, int height, int length)
{ {
@@ -122,4 +123,14 @@ public class PartitionNode extends BoundingBox
return this; return this;
} }
} }
public void setData(T value)
{
this.data = value;
}
public T getData()
{
return data;
}
} }

View File

@@ -0,0 +1,133 @@
package StevenDimDoors.experimental;
import java.util.ArrayList;
public class RoomData
{
/* Implementation Note:
* Plans for links between rooms are stored in lists rather than a graph,
* even though they duplicate some graph functionality, because there are
* relatively few of them compared to the number of rooms. Moreover, some
* links don't even have destinations because they're intended to lead to
* other dungeons.
*/
private int capacity;
private int distance;
private boolean decayed;
private PartitionNode partitionNode;
private ArrayList<LinkPlan> inboundLinks;
private ArrayList<LinkPlan> outboundLinks;
private DirectedGraph<RoomData, DoorwayData> layout;
private IGraphNode<RoomData, DoorwayData> layoutNode;
public RoomData(PartitionNode partitionNode)
{
this.partitionNode = partitionNode;
this.inboundLinks = new ArrayList<LinkPlan>();
this.outboundLinks = new ArrayList<LinkPlan>();
this.layoutNode = null;
this.layout = null;
this.distance = -1;
this.capacity = -1;
this.decayed = false;
partitionNode.setData(this);
}
public PartitionNode getPartitionNode()
{
return this.partitionNode;
}
public IGraphNode<RoomData, DoorwayData> getLayoutNode()
{
return this.layoutNode;
}
public void addToLayout(DirectedGraph<RoomData, DoorwayData> layout)
{
this.layout = layout;
this.layoutNode = layout.addNode(this);
}
public boolean isDecayed()
{
return decayed;
}
public void setDecayed(boolean value)
{
this.decayed = value;
}
public ArrayList<LinkPlan> getInboundLinks()
{
return this.inboundLinks;
}
public ArrayList<LinkPlan> getOutboundLinks()
{
return this.outboundLinks;
}
public int getDistance()
{
return distance;
}
public void setDistance(int value)
{
distance = value;
}
public void remove()
{
// Remove the room from the partition tree and from the layout graph.
// Also remove any ancestors that become leaf nodes.
PartitionNode parent;
PartitionNode current = partitionNode;
while (current != null && current.isLeaf())
{
parent = current.parent();
current.remove();
current = parent;
}
// Remove the room from the layout graph
layout.removeNode(layoutNode);
// Remove any links
while (!inboundLinks.isEmpty())
inboundLinks.get(inboundLinks.size() - 1).remove();
while (!outboundLinks.isEmpty())
outboundLinks.get(outboundLinks.size() - 1).remove();
// Wipe the room's data, as a precaution
layout = null;
partitionNode = null;
inboundLinks = null;
outboundLinks = null;
}
public int estimateDoorCapacity()
{
if (capacity >= 0)
return capacity;
int cellsX = (partitionNode.width() - 3) / 2;
int cellsZ = (partitionNode.length() - 3) / 2;
capacity = Math.min(cellsX * cellsZ, 3);
return capacity;
}
public int getRemainingDoorCapacity()
{
return (estimateDoorCapacity() - outboundLinks.size());
}
public boolean isProtected()
{
return !inboundLinks.isEmpty() || !outboundLinks.isEmpty();
}
}

View File

@@ -0,0 +1,192 @@
package StevenDimDoors.experimental;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Random;
import java.util.Stack;
public class SectionData
{
// Specifies the chance of selecting a destination from protectedRooms
// rather than from destinationRooms (which will then become protected)
private static final int PROTECTED_DESTINATION_CHANCE = 4;
private static final int MAX_PROTECTED_DESTINATION_CHANCE = 5;
private int capacity;
private ArrayList<RoomData> sourceRooms;
private ArrayList<RoomData> protectedRooms;
private ArrayList<RoomData> destinationRooms;
private ArrayList<LinkPlan> reservations;
private SectionData(ArrayList<RoomData> sourceRooms, ArrayList<RoomData> destinationRooms, int capacity)
{
this.capacity = capacity;
this.sourceRooms = sourceRooms;
this.destinationRooms = destinationRooms;
this.protectedRooms = new ArrayList<RoomData>();
this.reservations = new ArrayList<LinkPlan>();
}
public static SectionData createFromCore(IGraphNode<RoomData, DoorwayData> core)
{
int capacity = 0;
ArrayList<RoomData> sourceRooms = new ArrayList<RoomData>();
ArrayList<RoomData> destinationRooms = new ArrayList<RoomData>();
boolean hasHoles;
RoomData currentRoom;
IGraphNode<RoomData, DoorwayData> current;
IGraphNode<RoomData, DoorwayData> neighbor;
Stack<IGraphNode<RoomData, DoorwayData>> ordering = new Stack<IGraphNode<RoomData, DoorwayData>>();
HashSet<IGraphNode<RoomData, DoorwayData>> visited = new HashSet<IGraphNode<RoomData, DoorwayData>>();
visited.add(core);
ordering.add(core);
while (!ordering.isEmpty())
{
current = ordering.pop();
hasHoles = false;
for (IEdge<RoomData, DoorwayData> edge : current.outbound())
{
neighbor = edge.tail();
if (visited.add(neighbor))
{
ordering.add(neighbor);
}
}
for (IEdge<RoomData, DoorwayData> edge : current.inbound())
{
neighbor = edge.head();
if (visited.add(neighbor))
{
ordering.add(neighbor);
}
if (edge.data().axis() == DoorwayData.Y_AXIS)
{
hasHoles = true;
}
}
if (!hasHoles)
{
currentRoom = current.data();
destinationRooms.add(currentRoom);
if (currentRoom.estimateDoorCapacity() > 0)
{
capacity += currentRoom.estimateDoorCapacity();
sourceRooms.add(currentRoom);
}
}
}
return new SectionData(sourceRooms, destinationRooms, capacity);
}
public int capacity()
{
return capacity;
}
public void createEntranceLink(Random random)
{
int index = random.nextInt(sourceRooms.size());
RoomData room = sourceRooms.get(index);
LinkPlan.createEntranceLink(room);
if (room.getRemainingDoorCapacity() == 0)
{
sourceRooms.remove(index);
}
// It's okay to check containment in this list because
// the number of protected rooms is expected to be small
if (!protectedRooms.contains(room))
{
protectedRooms.add(room);
}
capacity--;
}
public void createDungeonLink(Random random)
{
int index = random.nextInt(sourceRooms.size());
RoomData room = sourceRooms.get(index);
LinkPlan.createDungeonLink(room);
if (room.getRemainingDoorCapacity() == 0)
{
sourceRooms.remove(index);
}
// It's okay to check containment in this list because
// the number of protected rooms is expected to be small
if (!protectedRooms.contains(room))
{
protectedRooms.add(room);
}
capacity--;
}
public void reserveSectionLink(SectionData destination, Random random)
{
// This method "reserves" a link by decrementing the capacity of this
// section and assigning a source room to the link. However, assigning
// its destination in a particular section is deferred. Why?
// We favor using source rooms as destinations to cut down the number
// of rooms that have to be marked as protected against decay effects.
// We defer assigning a destination until after all source rooms are
// known so that we have that information available. Otherwise,
// destination selection would be biased toward non-source rooms and
// rooms with dungeon doors, which are placed before section links.
int index = random.nextInt(sourceRooms.size());
RoomData room = sourceRooms.get(index);
destination.reserveDestination(LinkPlan.createInternalLink(room));
if (room.getRemainingDoorCapacity() == 0)
{
sourceRooms.remove(index);
}
// It's okay to check containment in this list because
// the number of protected rooms is expected to be small
if (!protectedRooms.contains(room))
{
protectedRooms.add(room);
}
capacity--;
}
public void processReservedLinks(Random random)
{
for (LinkPlan link : reservations)
{
link.setDestination( getLinkDestination(random) );
}
reservations.clear();
}
private void reserveDestination(LinkPlan link)
{
reservations.add(link);
}
private RoomData getLinkDestination(Random random)
{
RoomData destination;
// Choose whether to select a room that is already protected or select
// from all possible destination rooms. Note that some destination rooms
// may also be protected rooms already.
if (random.nextInt(MAX_PROTECTED_DESTINATION_CHANCE) < PROTECTED_DESTINATION_CHANCE)
{
destination = protectedRooms.get( random.nextInt(protectedRooms.size()) );
}
else
{
destination = destinationRooms.get( random.nextInt(destinationRooms.size()) );
// It's okay to check containment in this list because
// the number of protected rooms is expected to be small
if (!protectedRooms.contains(destination))
{
protectedRooms.add(destination);
}
}
return destination;
}
}

View File

@@ -0,0 +1,17 @@
package StevenDimDoors.experimental.decorators;
import java.util.Random;
import net.minecraft.world.World;
import StevenDimDoors.experimental.RoomData;
import StevenDimDoors.mod_pocketDim.Point3D;
import StevenDimDoors.mod_pocketDim.config.DDProperties;
public abstract class BaseDecorator
{
public BaseDecorator() { }
public abstract boolean canDecorate(RoomData room);
public abstract void decorate(RoomData room, World world, Point3D offset, Random random, DDProperties properties);
}

View File

@@ -0,0 +1,51 @@
package StevenDimDoors.experimental.decorators;
import java.util.ArrayList;
import java.util.Random;
import StevenDimDoors.experimental.RoomData;
public class DecoratorFinder
{
private static ArrayList<BaseDecorator> decorators = null;
private DecoratorFinder() { }
public static BaseDecorator find(RoomData room, Random random)
{
if (decorators == null)
{
load();
}
// Since there are only a few decorators right now, we just iterate
// over the list and check them all. If we add a lot, we'll need to
// switch to a more efficient approach.
ArrayList<BaseDecorator> matches = new ArrayList<BaseDecorator>();
for (BaseDecorator decorator : decorators)
{
if (decorator.canDecorate(room))
{
matches.add(decorator);
}
}
if (matches.isEmpty())
{
return null;
}
else
{
return matches.get( random.nextInt(matches.size()) );
}
}
private static void load()
{
// List all the decorators we have
decorators = new ArrayList<BaseDecorator>();
decorators.add(new LinkDestinationDecorator());
decorators.add(new DefaultDoorDecorator());
decorators.add(new TorchDecorator());
}
}

View File

@@ -0,0 +1,49 @@
package StevenDimDoors.experimental.decorators;
import java.util.Random;
import net.minecraft.item.ItemDoor;
import net.minecraft.world.World;
import StevenDimDoors.experimental.LinkPlan;
import StevenDimDoors.experimental.RoomData;
import StevenDimDoors.mod_pocketDim.Point3D;
import StevenDimDoors.mod_pocketDim.mod_pocketDim;
import StevenDimDoors.mod_pocketDim.config.DDProperties;
public class DefaultDoorDecorator extends BaseDecorator
{
@Override
public boolean canDecorate(RoomData room)
{
return !room.getOutboundLinks().isEmpty();
}
@Override
public void decorate(RoomData room, World world, Point3D offset, Random random, DDProperties properties)
{
// TODO: This is just an improvised implementation for testing
Point3D corner = room.getPartitionNode().minCorner().clone();
corner.add(offset);
int count = 0;
Point3D source = null;
for (LinkPlan plan : room.getOutboundLinks())
{
source = new Point3D(corner.getX() + 2, corner.getY() + 2, corner.getZ() + count + 1);
ItemDoor.placeDoorBlock(world, source.getX(), source.getY() - 1, source.getZ(), 0, mod_pocketDim.dimensionalDoor);
plan.setSourcePoint(source);
count++;
}
if (source == null)
{
throw new IllegalStateException("This should never happen because this decorator only applies if outbound links exist!");
}
for (LinkPlan plan : room.getInboundLinks())
{
plan.setDestinationPoint(source);
}
}
}

View File

@@ -0,0 +1,37 @@
package StevenDimDoors.experimental.decorators;
import java.util.Random;
import net.minecraft.world.World;
import StevenDimDoors.experimental.LinkPlan;
import StevenDimDoors.experimental.PartitionNode;
import StevenDimDoors.experimental.RoomData;
import StevenDimDoors.mod_pocketDim.Point3D;
import StevenDimDoors.mod_pocketDim.config.DDProperties;
public class LinkDestinationDecorator extends BaseDecorator
{
@Override
public boolean canDecorate(RoomData room)
{
return room.getOutboundLinks().isEmpty() && !room.getInboundLinks().isEmpty();
}
@Override
public void decorate(RoomData room, World world, Point3D offset, Random random, DDProperties properties)
{
// Set the center of the room as the destination for all inbound links
PartitionNode<RoomData> partition = room.getPartitionNode();
Point3D destination = partition.minCorner().clone();
destination.add(
offset.getX() + partition.width() / 2,
offset.getY() + 2,
offset.getZ() + partition.length() / 2);
for (LinkPlan plan : room.getInboundLinks())
{
plan.setDestinationPoint(destination);
}
}
}

View File

@@ -0,0 +1,86 @@
package StevenDimDoors.experimental.decorators;
import java.util.Random;
import net.minecraft.block.Block;
import net.minecraft.util.MathHelper;
import net.minecraft.world.World;
import StevenDimDoors.experimental.PartitionNode;
import StevenDimDoors.experimental.RoomData;
import StevenDimDoors.mod_pocketDim.Point3D;
import StevenDimDoors.mod_pocketDim.config.DDProperties;
public class TorchDecorator extends BaseDecorator
{
@Override
public boolean canDecorate(RoomData room)
{
return !room.isProtected();
}
@Override
public void decorate(RoomData room, World world, Point3D offset, Random random, DDProperties properties)
{
// SenseiKiwi: Place a single random torch along the walls.
// We could do more complex arrangements but I feel that a single
// torches here and there will be a little unsettling.
// The walls might be broken by passages or decay, so this will
// require trial and error.
final int MAX_ATTEMPTS = 5;
int x;
int z;
int attempts = 0;
PartitionNode<RoomData> partition = room.getPartitionNode();
int minX = partition.minCorner().getX() + offset.getX();
int minZ = partition.minCorner().getZ() + offset.getZ();
int maxX = partition.maxCorner().getX() + offset.getX();
int maxZ = partition.maxCorner().getZ() + offset.getZ();
int torchLevel = partition.minCorner().getY() + offset.getY() + 2;
for (; attempts < MAX_ATTEMPTS; attempts++)
{
// Choose a random side of the room to place the torch. The sides are numbered arbitrarily here.
// Then choose a random position along the wall and check if there is a block there to place the
// torch against. We assume that all blocks are bricks and thus valid.
switch (random.nextInt(4))
{
case 0: // Positive X side
z = MathHelper.getRandomIntegerInRange(random, minZ + 1, maxZ - 1);
if (!world.isAirBlock(maxX, torchLevel, z))
{
world.setBlock(maxX - 1, torchLevel, z, Block.torchWood.blockID, 2, 0);
return;
}
break;
case 1: // Negative X side
z = MathHelper.getRandomIntegerInRange(random, minZ + 1, maxZ - 1);
if (!world.isAirBlock(minX, torchLevel, z))
{
world.setBlock(minX + 1, torchLevel, z, Block.torchWood.blockID, 1, 0);
return;
}
break;
case 2: // Positive Z side
x = MathHelper.getRandomIntegerInRange(random, minX + 1, maxX - 1);
if (!world.isAirBlock(x, torchLevel, maxZ))
{
world.setBlock(x, torchLevel, maxZ - 1, Block.torchWood.blockID, 4, 0);
return;
}
break;
case 3: // Negative Z side
x = MathHelper.getRandomIntegerInRange(random, minX + 1, maxX - 1);
if (!world.isAirBlock(x, torchLevel, minZ))
{
world.setBlock(x, torchLevel, minZ + 1, Block.torchWood.blockID, 3, 0);
return;
}
break;
}
}
}
}

View File

@@ -55,6 +55,20 @@ public class Point3D implements Serializable {
{ {
return this.z = z; return this.z = z;
} }
public void add(int x, int y, int z)
{
this.x += x;
this.y += y;
this.z += z;
}
public void add(Point3D other)
{
this.x += other.x;
this.y += other.y;
this.z += other.z;
}
@Override @Override
public Point3D clone() public Point3D clone()

View File

@@ -465,6 +465,7 @@ public class PocketBuilder
Point3D door = new Point3D(x, y, z); Point3D door = new Point3D(x, y, z);
BlockRotator.transformPoint(center, door, orientation - BlockRotator.EAST_DOOR_METADATA, door); BlockRotator.transformPoint(center, door, orientation - BlockRotator.EAST_DOOR_METADATA, door);
/*
//Build the outer layer of Eternal Fabric //Build the outer layer of Eternal Fabric
buildBox(world, center.getX(), center.getY(), center.getZ(), (size / 2), properties.PermaFabricBlockID, false, 0); buildBox(world, center.getX(), center.getY(), center.getZ(), (size / 2), properties.PermaFabricBlockID, false, 0);
@@ -474,8 +475,9 @@ public class PocketBuilder
buildBox(world, center.getX(), center.getY(), center.getZ(), (size / 2) - layer, properties.FabricBlockID, buildBox(world, center.getX(), center.getY(), center.getZ(), (size / 2) - layer, properties.FabricBlockID,
layer < (wallThickness - 1) && properties.TNFREAKINGT_Enabled, properties.NonTntWeight); layer < (wallThickness - 1) && properties.TNFREAKINGT_Enabled, properties.NonTntWeight);
} }
*/
//MazeBuilder.generate(world, x, y, z, random); MazeBuilder.generate(world, x, y, z, random, properties);
//Build the door //Build the door
int doorOrientation = BlockRotator.transformMetadata(BlockRotator.EAST_DOOR_METADATA, orientation - BlockRotator.EAST_DOOR_METADATA + 2, properties.DimensionalDoorID); int doorOrientation = BlockRotator.transformMetadata(BlockRotator.EAST_DOOR_METADATA, orientation - BlockRotator.EAST_DOOR_METADATA + 2, properties.DimensionalDoorID);