package squidpony.squidgrid.mapping;
import squidpony.ArrayTools;
import squidpony.annotation.Beta;
import squidpony.squidmath.*;
import java.util.*;
/**
* Generator for maps of high-tech areas like space stations or starships, with repeated modules laid out in random ways.
* Different from traditional fantasy dungeon generation in that it should seem generally less chaotic in how it's laid
* out, and repeated elements with minor tweaks should be especially common. May also be useful in fantasy games for
* regimented areas built by well-organized military forces.
* <br>
* Preview: https://gist.github.com/tommyettinger/c711f8fc83fa9919245d88092444bf7f
* Created by Tommy Ettinger on 4/2/2016.
*/
@Beta
public class ModularMapGenerator {
public DungeonUtility utility;
protected int height, width;
public StatefulRNG rng;
protected long rebuildSeed;
protected boolean seedFixed = false;
protected char[][] map = null;
protected int[][] environment = null;
//public RegionMap<MapModule> layout, modules, inverseModules;
public RegionMap<MapModule> layout;
public OrderedMap<Integer, ArrayList<MapModule>> modules;
public OrderedMap<Coord, MapModule> displacement;
private void putModule(short[] module) {
char[][] unp = CoordPacker.unpackChar(module, '.', '#');
MapModule mm = new MapModule(ArrayTools.insert(unp,
ArrayTools.fill('#', unp.length + 2, unp[0].length + 2), 1, 1));
//short[] b = CoordPacker.rectangle(1 + mm.max.x, 1 + mm.max.y);
//modules.put(b, mm);
//inverseModules.put(CoordPacker.negatePacked(b), mm);
ArrayList<MapModule> mms = modules.get(mm.category);
if (mms == null) {
mms = new ArrayList<>(16);
mms.add(mm);
modules.put(mm.category, mms);
} else
mms.add(mm);
}
private void putRectangle(int width, int height, float multiplier) {
putModule(CoordPacker.rectangle(Math.round(width * multiplier), Math.round(height * multiplier)));
}
private void putCircle(int radius, float multiplier) {
putModule(CoordPacker.circle(Coord.get(Math.round(radius * multiplier + 1), Math.round(radius * multiplier + 1)),
Math.round(radius * multiplier),
Math.round((radius + 1) * 2 * multiplier + 1), Math.round((radius + 1) * 2 * multiplier + 1)));
}
private void initModules() {
layout = new RegionMap<>(64);
//modules = new RegionMap<>(64);
//inverseModules = new RegionMap<>(64);
modules = new OrderedMap<>(64);
for (int i = 1; i <= 64; i <<= 1) {
ArrayList<MapModule> mms = new ArrayList<>(16);
modules.put(i, mms);
}
displacement = new OrderedMap<>(64);
float multiplier = 1;//(float) Math.sqrt(Math.max(1f, Math.min(width, height) / 24f));
putRectangle(2, 2, multiplier);
putRectangle(3, 3, multiplier);
putRectangle(4, 4, multiplier);
putRectangle(4, 2, multiplier);
putRectangle(2, 4, multiplier);
putRectangle(6, 6, multiplier);
putRectangle(6, 3, multiplier);
putRectangle(3, 6, multiplier);
putCircle(2, multiplier);
putRectangle(8, 8, multiplier);
putRectangle(6, 12, multiplier);
putRectangle(12, 6, multiplier);
putCircle(4, multiplier);
putRectangle(14, 14, multiplier);
putRectangle(9, 18, multiplier);
putRectangle(18, 9, multiplier);
putRectangle(14, 18, multiplier);
putRectangle(18, 14, multiplier);
putCircle(7, multiplier);
}
/**
* Make a ModularMapGenerator with a StatefulRNG (backed by LightRNG) using a random seed, height 30, and width 60.
*/
public ModularMapGenerator() {
this(60, 30);
}
/**
* Make a ModularMapGenerator with the given height and width; the RNG used for generating a dungeon and
* adding features will be a StatefulRNG (backed by LightRNG) using a random seed.
*
* @param width The width of the dungeon in cells
* @param height The height of the dungeon in cells
*/
public ModularMapGenerator(int width, int height) {
this(width, height, new StatefulRNG());
}
/**
* Make a ModularMapGenerator with the given height, width, and RNG. Use this if you want to seed the RNG.
*
* @param width The width of the dungeon in cells
* @param height The height of the dungeon in cells
* @param rng The RNG to use for all purposes in this class; if it is a StatefulRNG, then it will be used as-is,
* but if it is not a StatefulRNG, a new StatefulRNG will be used, randomly seeded by this parameter
*/
public ModularMapGenerator(int width, int height, RNG rng) {
this.rng = (rng instanceof StatefulRNG) ? (StatefulRNG) rng : new StatefulRNG(rng.nextLong());
utility = new DungeonUtility(this.rng);
rebuildSeed = this.rng.getState();
this.height = height;
this.width = width;
map = new char[width][height];
environment = new int[width][height];
for (int x = 0; x < this.width; x++) {
Arrays.fill(map[x], '#');
}
initModules();
}
/**
* Copies all fields from copying and makes a new DungeonGenerator.
*
* @param copying the DungeonGenerator to copy
*/
public ModularMapGenerator(ModularMapGenerator copying) {
rng = new StatefulRNG(copying.rng.getState());
utility = new DungeonUtility(rng);
rebuildSeed = rng.getState();
height = copying.height;
width = copying.width;
map = ArrayTools.copy(copying.map);
environment = ArrayTools.copy(copying.environment);
layout = new RegionMap<>(copying.layout);
modules = new OrderedMap<>(copying.modules);
}
/**
* Get the most recently generated char[][] map out of this class. The
* map may be null if generate() or setMap() have not been called.
*
* @return a char[][] map, or null.
*/
public char[][] getMap() {
return map;
}
/**
* Get the most recently generated char[][] map out of this class without any chars other than '#' or '.', for
* walls and floors respectively. The map may be null if generate() or setMap() have not been called.
*
* @return a char[][] map with only '#' for walls and '.' for floors, or null.
*/
public char[][] getBareMap() {
return DungeonUtility.simplifyDungeon(map);
}
public char[][] generate() {
MapModule mm, mm2;
int xPos, yPos, categorySize = 32, alteredSize = (categorySize * 3) >>> 1, bits = 5, ctr;
Coord[][] grid = new Coord[1][1];
// find biggest category and drop down as many modules as we can fit
for (; categorySize >= 4; categorySize >>= 1, alteredSize = (categorySize * 3) >>> 1, bits--) {
if (width / alteredSize <= 0 || height / alteredSize <= 0)
continue;
grid = new Coord[width / alteredSize][height / alteredSize];
ctr = 0;
for (int xLimit = alteredSize - 1, x = 0; xLimit < width; xLimit += alteredSize, x += alteredSize) {
for (int yLimit = alteredSize - 1, y = 0; yLimit < height; yLimit += alteredSize, y += alteredSize) {
if (layout.allAt(x + alteredSize / 2, y + alteredSize / 2).isEmpty()) // && (bits <= 3 || rng.nextInt(5) < bits)
{
if (rng.between(2, grid.length * grid[0].length + 3) == ctr++)
continue;
mm = rng.getRandomElement(modules.get(categorySize));
if (mm == null) break;
mm = mm.rotate(rng.nextInt(4));
xPos = rng.nextInt(3) << (bits - 2);
yPos = rng.nextInt(3) << (bits - 2);
for (int px = 0; px <= mm.max.x; px++) {
System.arraycopy(mm.map[px], 0, map[px + x + xPos], y + yPos, mm.max.y + 1);
System.arraycopy(mm.environment[px], 0, environment[px + x + xPos], y + yPos, mm.max.y + 1);
}
layout.put(CoordPacker.rectangle(x + xPos, y + yPos, categorySize, categorySize), mm);
displacement.put(Coord.get(x + xPos, y + yPos), mm);
grid[x / alteredSize][y / alteredSize] = Coord.get(x + xPos, y + yPos);
}
}
}
if (!layout.isEmpty())
break;
}
Coord a, b;
int gw = grid.length;
if (gw > 0) {
int gh = grid[0].length;
for (int w = 0; w < gw; w++) {
for (int h = 0; h < gh; h++) {
a = grid[w][h];
if (a == null)
continue;
int connectors = rng.nextInt(16) | rng.nextInt(16);
if ((connectors & 1) == 1 && w > 0 && grid[w - 1][h] != null) {
b = grid[w - 1][h];
connectLeftRight(displacement.get(b), b.x, b.y, displacement.get(a), a.x, a.y);
}
if ((connectors & 2) == 2 && w < gw - 1 && grid[w + 1][h] != null) {
b = grid[w + 1][h];
connectLeftRight(displacement.get(a), a.x, a.y, displacement.get(b), b.x, b.y);
}
if ((connectors & 4) == 4 && h > 0 && grid[w][h - 1] != null) {
b = grid[w][h - 1];
connectTopBottom(displacement.get(b), b.x, b.y, displacement.get(a), a.x, a.y);
}
if ((connectors & 8) == 8 && h < gh - 1 && grid[w][h + 1] != null) {
b = grid[w][h + 1];
connectTopBottom(displacement.get(a), a.x, a.y, displacement.get(b), b.x, b.y);
}
}
}
}
Coord begin;
short[] packed;
for (Map.Entry<Coord, MapModule> dmm : displacement.entrySet()) {
begin = dmm.getKey();
mm = dmm.getValue();
//int newCat = mm.category;
//if(newCat >= 16) newCat >>>= 1;
//if(newCat >= 8) newCat >>>= 1;
//mm2 = rng.getRandomElement(modules.get(newCat));
int shiftsX = begin.x - (mm.category * 3 / 2) * ((begin.x * 2) / (3 * mm.category)),
shiftsY = begin.y - (mm.category * 3 / 2) * ((begin.y * 2) / (3 * mm.category)),
leftSize = Integer.highestOneBit(shiftsX),
rightSize = Integer.highestOneBit((mm.category >>> 1) - shiftsX),
topSize = Integer.highestOneBit(shiftsY),
bottomSize = Integer.highestOneBit((mm.category >>> 1) - shiftsY);
if (leftSize >= 4 && !mm.leftDoors.isEmpty()) {
mm2 = rng.getRandomElement(modules.get(leftSize));
if (mm2 == null) continue;
if (mm2.rightDoors.isEmpty()) {
if (!mm2.topDoors.isEmpty())
mm2 = mm2.rotate(1);
else if (!mm2.leftDoors.isEmpty())
mm2 = mm2.flip(true, false);
else if (!mm2.bottomDoors.isEmpty())
mm2 = mm2.rotate(3);
else continue;
}
for (int i = 0; i < 4; i++) {
packed = CoordPacker.rectangle(begin.x - shiftsX, begin.y + i * mm.category / 4, leftSize, leftSize);
if (layout.containsRegion(packed))
continue;
for (int px = 0; px <= mm2.max.x; px++) {
System.arraycopy(mm2.map[px], 0, map[px + begin.x - shiftsX], begin.y + i * mm.category / 4, mm2.max.y + 1);
System.arraycopy(mm2.environment[px], 0, environment[px + begin.x - shiftsX], begin.y + i * mm.category / 4, mm2.max.y + 1);
}
layout.put(packed, mm2);
connectLeftRight(mm2, begin.x - shiftsX, begin.y + i * mm.category / 4, mm, begin.x, begin.y);
}
}
if (rightSize >= 4 && !mm.rightDoors.isEmpty()) {
mm2 = rng.getRandomElement(modules.get(rightSize));
if (mm2 == null) continue;
if (mm2.leftDoors.isEmpty()) {
if (!mm2.bottomDoors.isEmpty())
mm2 = mm2.rotate(1);
else if (!mm2.rightDoors.isEmpty())
mm2 = mm2.flip(true, false);
else if (!mm2.topDoors.isEmpty())
mm2 = mm2.rotate(3);
else continue;
}
for (int i = 0; i < 4; i++) {
packed = CoordPacker.rectangle(begin.x + mm.category, begin.y + i * mm.category / 4, rightSize, rightSize);
if (layout.containsRegion(packed))
continue;
for (int px = 0; px <= mm2.max.x; px++) {
System.arraycopy(mm2.map[px], 0, map[px + begin.x + mm.category], begin.y + i * mm.category / 4, mm2.max.y + 1);
System.arraycopy(mm2.environment[px], 0, environment[px + begin.x + mm.category], begin.y + i * mm.category / 4, mm2.max.y + 1);
}
layout.put(packed, mm2);
connectLeftRight(mm, begin.x, begin.y, mm2, begin.x + mm.category, begin.y + i * mm.category / 4);
}
}
if (topSize >= 4 && !mm.topDoors.isEmpty()) {
mm2 = rng.getRandomElement(modules.get(topSize));
if (mm2 == null) continue;
if (mm2.bottomDoors.isEmpty()) {
if (!mm2.leftDoors.isEmpty())
mm2 = mm2.rotate(3);
else if (!mm2.topDoors.isEmpty())
mm2 = mm2.flip(false, true);
else if (!mm2.rightDoors.isEmpty())
mm2 = mm2.rotate(1);
else continue;
}
for (int i = 0; i < 4; i++) {
packed = CoordPacker.rectangle(begin.x + i * mm.category / 4, begin.y - shiftsY, topSize, topSize);
if (layout.containsRegion(packed))
continue;
for (int px = 0; px <= mm2.max.x; px++) {
System.arraycopy(mm2.map[px], 0, map[px + begin.x + i * mm.category / 4], begin.y - shiftsY, mm2.max.y + 1);
System.arraycopy(mm2.environment[px], 0, environment[px + begin.x + i * mm.category / 4], begin.y - shiftsY, mm2.max.y + 1);
}
layout.put(packed, mm2);
connectTopBottom(mm2, begin.x + i * mm.category / 4, begin.y - shiftsY, mm, begin.x, begin.y);
}
}
if (bottomSize >= 4 && !mm.bottomDoors.isEmpty()) {
mm2 = rng.getRandomElement(modules.get(bottomSize));
if (mm2 == null) continue;
if (mm2.topDoors.isEmpty()) {
if (!mm2.rightDoors.isEmpty())
mm2 = mm2.rotate(1);
else if (!mm2.topDoors.isEmpty())
mm2 = mm2.flip(false, true);
else if (!mm2.leftDoors.isEmpty())
mm2 = mm2.rotate(3);
else continue;
}
for (int i = 0; i < 4; i++) {
packed = CoordPacker.rectangle(begin.x + i * mm.category / 4, begin.y + mm.category, bottomSize, bottomSize);
if (layout.containsRegion(packed))
continue;
for (int px = 0; px <= mm2.max.x; px++) {
System.arraycopy(mm2.map[px], 0, map[px + begin.x + i * mm.category / 4], begin.y + mm.category, mm2.max.y + 1);
System.arraycopy(mm2.environment[px], 0, environment[px + begin.x + i * mm.category / 4], begin.y + mm.category, mm2.max.y + 1);
}
layout.put(packed, mm2);
connectTopBottom(mm, begin.x, begin.y, mm2, begin.x + i * mm.category / 4, begin.y + mm.category);
}
}
}
return map;
}
/**
* Change the underlying char[][]; only affects the toString method, and of course getMap
*
* @param map a char[][], probably produced by an earlier call to this class and then modified.
*/
public void setMap(char[][] map) {
this.map = map;
if (map == null) {
width = 0;
height = 0;
return;
}
width = map.length;
if (width > 0)
height = map[0].length;
}
/**
* Height of the map in cells.
*
* @return Height of the map in cells.
*/
public int getHeight() {
return height;
}
/**
* Width of the map in cells.
*
* @return Width of the map in cells.
*/
public int getWidth() {
return width;
}
/**
* Gets the environment int 2D array for use with classes like RoomFinder.
*
* @return the environment int 2D array
*/
public int[][] getEnvironment() {
return environment;
}
/**
* Sets the environment int 2D array.
*
* @param environment a 2D array of int, where each int corresponds to a constant in MixedGenerator.
*/
public void setEnvironment(int[][] environment) {
this.environment = environment;
}
private void connectLeftRight(MapModule left, int leftX, int leftY, MapModule right, int rightX, int rightY) {
if (left.rightDoors == null || left.rightDoors.isEmpty()
|| right.leftDoors == null || right.leftDoors.isEmpty())
return;
List<Coord> line = new ArrayList<>(1), temp;
int best = 1024;
Coord tl, tr, twl, twr, wl = null, wr = null;
for (Coord l : left.rightDoors) {
tl = twl = l.translate(leftX, leftY);
if (tl.x > 0 && tl.x < width - 1 && map[tl.x - 1][tl.y] != '#')
tl = Coord.get(tl.x + 1, tl.y);
else if (tl.x > 0 && tl.x < width - 1 && map[tl.x + 1][tl.y] != '#')
tl = Coord.get(tl.x - 1, tl.y);
else if (tl.y > 0 && tl.y < height - 1 && map[tl.x][tl.y - 1] != '#')
tl = Coord.get(tl.x, tl.y + 1);
else if (tl.y > 0 && tl.y < height - 1 && map[tl.x][tl.y + 1] != '#')
tl = Coord.get(tl.x, tl.y - 1);
else
continue;
for (Coord r : right.leftDoors) {
tr = twr = r.translate(rightX, rightY);
if (tr.x > 0 && tr.x < width - 1 && map[tr.x - 1][tr.y] != '#')
tr = Coord.get(tr.x + 1, tr.y);
else if (tr.x > 0 && tr.x < width - 1 && map[tr.x + 1][tr.y] != '#')
tr = Coord.get(tr.x - 1, tr.y);
else if (tr.y > 0 && tr.y < height - 1 && map[tr.x][tr.y - 1] != '#')
tr = Coord.get(tr.x, tr.y + 1);
else if (tr.y > 0 && tr.y < height - 1 && map[tr.x][tr.y + 1] != '#')
tr = Coord.get(tr.x, tr.y - 1);
else
continue;
temp = OrthoLine.line(tl, tr);
if (temp.size() < best) {
line = temp;
best = line.size();
wl = twl;
wr = twr;
}
}
}
temp = new ArrayList<>(line.size());
for (Coord c : line) {
if (map[c.x][c.y] == '#') {
map[c.x][c.y] = '.';
environment[c.x][c.y] = MixedGenerator.CORRIDOR_FLOOR;
temp.add(c);
}
}
if (wl != null && map[wl.x][wl.y] == '#') {
//if(line.isEmpty())
map[wl.x][wl.y] = '.';
environment[wl.x][wl.y] = MixedGenerator.ROOM_FLOOR;
//else
// map[wl.x][wl.y] = '+';
}
if (wr != null && map[wr.x][wr.y] == '#') {
//if(line.isEmpty())
map[wr.x][wr.y] = '.';
environment[wr.x][wr.y] = MixedGenerator.ROOM_FLOOR;
//else
// map[wr.x][wr.y] = '+';
}
layout.put(CoordPacker.packSeveral(temp), null);
}
private void connectTopBottom(MapModule top, int topX, int topY, MapModule bottom, int bottomX, int bottomY) {
if (top.bottomDoors == null || top.bottomDoors.isEmpty()
|| bottom.topDoors == null || bottom.topDoors.isEmpty())
return;
List<Coord> line = new ArrayList<>(1), temp;
int best = 1024;
Coord tt, tb, twt, twb, wt = null, wb = null;
for (Coord l : top.bottomDoors) {
tt = twt = l.translate(topX, topY);
if (tt.y > 0 && tt.y < height - 1 && map[tt.x][tt.y - 1] != '#')
tt = Coord.get(tt.x, tt.y + 1);
else if (tt.y > 0 && tt.y < height - 1 && map[tt.x][tt.y + 1] != '#')
tt = Coord.get(tt.x, tt.y - 1);
else if (tt.x > 0 && tt.x < width - 1 && map[tt.x - 1][tt.y] != '#')
tt = Coord.get(tt.x + 1, tt.y);
else if (tt.x > 0 && tt.x < width - 1 && map[tt.x + 1][tt.y] != '#')
tt = Coord.get(tt.x - 1, tt.y);
else
continue;
for (Coord r : bottom.topDoors) {
tb = twb = r.translate(bottomX, bottomY);
if (tb.y > 0 && tb.y < height - 1 && map[tb.x][tb.y - 1] != '#')
tb = Coord.get(tb.x, tb.y + 1);
else if (tb.y > 0 && tb.y < height - 1 && map[tb.x][tb.y + 1] != '#')
tb = Coord.get(tb.x, tb.y - 1);
else if (tb.x > 0 && tb.x < width - 1 && map[tb.x - 1][tb.y] != '#')
tb = Coord.get(tb.x + 1, tb.y);
else if (tb.x > 0 && tb.x < width - 1 && map[tb.x + 1][tb.y] != '#')
tb = Coord.get(tb.x - 1, tb.y);
else
continue;
temp = OrthoLine.line(tt, tb);
if (temp.size() < best) {
line = temp;
best = line.size();
wt = twt;
wb = twb;
}
}
}
temp = new ArrayList<>(line.size());
for (Coord c : line) {
if (map[c.x][c.y] == '#') {
map[c.x][c.y] = '.';
environment[c.x][c.y] = MixedGenerator.CORRIDOR_FLOOR;
temp.add(c);
}
}
if (wt != null && map[wt.x][wt.y] == '#') {
//if(line.isEmpty())
map[wt.x][wt.y] = '.';
environment[wt.x][wt.y] = MixedGenerator.ROOM_FLOOR;
//else
// map[wl.x][wl.y] = '+';
}
if (wb != null && map[wb.x][wb.y] == '#') {
//if(line.isEmpty())
map[wb.x][wb.y] = '.';
environment[wb.x][wb.y] = MixedGenerator.ROOM_FLOOR;
//else
// map[wb.x][wb.y] = '+';
}
layout.put(CoordPacker.packSeveral(temp), null);
}
}