package squidpony.squidgrid.mapping;
import squidpony.ArrayTools;
import squidpony.annotation.Beta;
import squidpony.squidmath.Arrangement;
import squidpony.squidmath.Coord;
import squidpony.squidmath.GreasedRegion;
import squidpony.squidmath.IntVLA;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
/**
* A Map-like collection that allows storing subdivisions of a 2D array with names (always Strings) and
* identifying numbers, then looking up {@link Coord}s to find the associated name and/or number, or or looking up
* a subdivision with a name or number to get a {@link GreasedRegion} back. This also stores connections between
* sections, which can be useful as part of a graph-like algorithm. It is fed the information it needs by a
* {@link RoomFinder} instance passed to the constructor or to {@link #reinitialize(RoomFinder)}. A RoomFinder is ready
* for usage after generating any dungeon with {@link SectionDungeonGenerator} or one of its subclasses; the field
* {@link SectionDungeonGenerator#finder} should usually be all that needs to be given to this class. If you don't use
* SectionDungeonGenerator, there's no reason you can't construct a RoomFinder independently and pass that (they can
* take a little time to construct on large or very complex maps, but shouldn't be heavy after construction).
* <br>
* If your code uses the {@link squidpony.squidgrid.zone.Zone} interface, then the {@link GreasedRegion} objects this
* can return do implement Zone, {@link squidpony.squidgrid.zone.MutableZone}, and {@link Iterable} of Coord.
* GreasedRegion is significantly faster than the alternatives ({@link squidpony.squidmath.CoordPacker} and manual
* Lists of Coord) for the spatial manipulations that RoomFinder needs to do to find room-like shapes, and this just
* gets its GreasedRegion values from RoomFinder directly.
* <br>
* Not to be confused with {@link squidpony.squidmath.RegionMap}, which has different functionality and a different
* pupose; RegionMap simply is a slight extension on OrderedMap to conveniently handle regions as short arrays produced
* by {@link squidpony.squidmath.CoordPacker}, while this class offers additional features for labeling and looking up
* sections of a map that were found by a {@link RoomFinder}.
* <br>
* Created by Tommy Ettinger on 11/28/2016.
*/
@Beta
public class SectionMap implements Serializable {
private static final long serialVersionUID = -2322572367863327331L;
protected int[][] map;
protected Arrangement<String> names;
protected ArrayList<GreasedRegion> regions;
protected ArrayList<IntVLA> connections;
/**
* This shouldn't usually be used unless you for some reason need to construct a SectionMap before you have access
* to a dungeon for it to map. If you do need this, then you must call {@link #reinitialize(RoomFinder)} to get any
* use out of this object.
* @see #SectionMap(RoomFinder) The preferred constructor, which takes a RoomFinder.
*/
public SectionMap()
{
map = new int[0][0];
names = new Arrangement<>(0);
regions = new ArrayList<>(0);
connections = new ArrayList<>(0);
}
/**
* The preferred constructor; takes a RoomFinder (often one already created in dungeon generation and available via
* {@link SectionDungeonGenerator#finder}) and uses it to give unique String names and identifying numbers to each
* room, corridor, and cave area that had been identified by that RoomFinder. In the rare but possible chance that
* a room, corridor, or cave overlaps with another such area, the one given the highest identifying number takes
* precedence, but this should probably only happen if RoomFinder was subclassed or its internal state was modified.
* Any cells that aren't a room, corridor, or cave (usually this contains all walls) are given identifying number 0,
* with the corresponding name "unused0." All other cells will then have positive, non-zero identifying numbers.
* Rooms are named next, starting at "room1" and going up to "room2" and so on until all rooms are named; the 1 in
* the name corresponds to the identifying number. After the last room has been found, e.g. "room5", then corridors
* are named, starting after the last room's number, so in the example that would be "corridor6", followed by
* "corridor7". The numbers in the names still correspond to identifying numbers. After corridors, caves follow the
* same pattern; in this example "cave8" would be followed by "cave9".
* @param rf a RoomFinder object; usually obtained via {@link SectionDungeonGenerator#finder}
*/
public SectionMap(RoomFinder rf)
{
if(rf == null)
{
map = new int[0][0];
names = new Arrangement<>(0);
regions = new ArrayList<>(0);
connections = new ArrayList<>(0);
return;
}
regions = new ArrayList<>(rf.rooms.size() + rf.caves.size() + rf.corridors.size());
names = new Arrangement<>(regions.size());
connections = new ArrayList<>(regions.size());
reinitialize(rf);
}
/**
* Copy constructor; takes an already-initialized SectionMap and deep-copies each element into this one. Allows null
* for {@code other}, which will make an empty SectionMap. This shouldn't be needed very often because SectionMap
* values are immutable, though their contents can in some cases be mutated independently, and this would allow one
* SectionMap to be copied and then have its items changed without changing the original.
* @param other a SectionMap to deep-copy into this one
*/
public SectionMap(SectionMap other)
{
if(other == null) {
map = new int[0][0];
names = new Arrangement<>(0);
regions = new ArrayList<>(0);
connections = new ArrayList<>(0);
return;
}
map = ArrayTools.copy(other.map);
names = new Arrangement<>(other.names);
regions = new ArrayList<>(other.regions.size());
connections = new ArrayList<>(other.connections.size());
for (int i = 0; i < other.connections.size(); i++) {
regions.add(new GreasedRegion(other.regions.get(i)));
connections.add(new IntVLA(other.connections.get(i)));
}
}
/**
* If this SectionMap hasn't been initialized or the map has completely changed (such as if the player went to a
* different floor of a dungeon), then you can call this method to avoid discarding some of the state from an
* earlier SectionMap. This does all the same steps {@link #SectionMap(RoomFinder)} does, so refer to that
* constructor's documentation for the names and numbers this assigns.
* @param rf a RoomFinder object; usually obtained via {@link SectionDungeonGenerator#finder}
* @return this for chaining.
*/
public SectionMap reinitialize(RoomFinder rf)
{
if(rf == null)
{
map = new int[0][0];
names = new Arrangement<>(0);
regions = new ArrayList<>(0);
connections = new ArrayList<>(0);
return this;
}
map = new int[rf.width][rf.height];
regions.clear();
names.clear();
connections.clear();
GreasedRegion t, all = new GreasedRegion(map, 0);
regions.add(all);
names.add("unused0");
connections.add(new IntVLA(0));
for (int i = 0; i < rf.rooms.size(); i++) {
t = rf.rooms.keyAt(i);
regions.add(t);
all.andNot(t);
t.writeIntsInto(map, names.size());
names.add("room"+names.size());
connections.add(new IntVLA(rf.rooms.getAt(i).size()));
}
for (int i = 0; i < rf.corridors.size(); i++) {
t = rf.corridors.keyAt(i);
regions.add(t);
all.andNot(t);
t.writeIntsInto(map, names.size());
names.add("corridor"+names.size());
connections.add(new IntVLA(rf.corridors.getAt(i).size()));
}
for (int i = 0; i < rf.caves.size(); i++) {
t = rf.caves.keyAt(i);
regions.add(t);
all.andNot(t);
t.writeIntsInto(map, names.size());
names.add("cave"+names.size());
connections.add(new IntVLA(rf.caves.getAt(i).size()));
}
int ls = 1;
List<GreasedRegion> connected;
IntVLA iv;
for (int i = 0; i < rf.rooms.size(); i++, ls++) {
connected = rf.rooms.getAt(i);
iv = connections.get(ls);
for (int j = 0; j < connected.size(); j++) {
iv.add(positionToNumber(connected.get(j).first()));
}
}
for (int i = 0; i < rf.corridors.size(); i++, ls++) {
connected = rf.corridors.getAt(i);
iv = connections.get(ls);
for (int j = 0; j < connected.size(); j++) {
iv.add(positionToNumber(connected.get(j).first()));
}
}
for (int i = 0; i < rf.caves.size(); i++, ls++) {
connected = rf.caves.getAt(i);
iv = connections.get(ls);
for (int j = 0; j < connected.size(); j++) {
iv.add(positionToNumber(connected.get(j).first()));
}
}
return this;
}
/**
* Gets the identifying number of the area that contains the given x, y position.
* @param x the x-coordinate to find the identifying number for; should be within bounds of the map
* @param y the y-coordinate to find the identifying number for; should be within bounds of the map
* @return the corresponding identifying number, or -1 if the parameters are invalid
*/
public int positionToNumber(int x, int y)
{
if(x < 0 || y < 0 || x >= map.length || y >= map[x].length)
return -1;
return map[x][y];
}
/**
* Gets the identifying number of the area that contains the given position.
* @param position the Coord to find the identifying number for; should be within bounds of the map and non-null
* @return the corresponding identifying number, or -1 if position is invalid or null
*/
public int positionToNumber(Coord position)
{
if(position == null)
return -1;
return positionToNumber(position.x, position.y);
}
/**
* Gets the name of the area that contains the given x, y position.
* @param x the x-coordinate to find the name for; should be within bounds of the map
* @param y the y-coordinate to find the name for; should be within bounds of the map
* @return the corresponding name as a String, or null if the parameters are invalid
*/
public String positionToName(int x, int y)
{
return numberToName(positionToNumber(x, y));
}
/**
* Gets the name of the area that contains the given position.
* @param position a Coord that should be within bounds of the map and non-null
* @return the corresponding name as a String, or null if position is invalid or null
*/
public String positionToName(Coord position)
{
if(position == null)
return null;
return numberToName(positionToNumber(position));
}
/**
* Gets the identifying number corresponding to the given name.
* @param name the name to look up, like "room1"
* @return the corresponding identifying number, or -1 if no such name exists
*/
public int nameToNumber(String name)
{
return names.getInt(name);
}
/**
* Gets the name that corresponds to the given identifying number.
* @param number the number to look up, like 1
* @return the corresponding name as a String, or null if no such number is used
*/
public String numberToName(int number)
{
return names.keyAt(number);
}
/**
* Gets the GreasedRegion that has the given identifying number.
* @param number the number to look up, like 1
* @return the corresponding GreasedRegion, or null if no such number is used
*/
public GreasedRegion numberToRegion(int number)
{
if(number < 0 || number >= regions.size())
return null;
return regions.get(number);
}
/**
* Gets the GreasedRegion that has the given name.
* @param name the name to look up, like "room1"
* @return the corresponding GreasedRegion, or null if no such name exists
*/
public GreasedRegion nameToRegion(String name)
{
return numberToRegion(nameToNumber(name));
}
/**
* Gets the GreasedRegion (a group of points as made by the constructor) that contains the given x, y point.
* @param x the x-coordinate to find the containing region for; should be within bounds of the map
* @param y the y-coordinate to find the containing region for; should be within bounds of the map
* @return the GreasedRegion containing the given point, or null if the parameters are invalid
*/
public GreasedRegion positionToContaining(int x, int y)
{
return numberToRegion(positionToNumber(x, y));
}
/**
* Gets the GreasedRegion (a group of points as made by the constructor) that contains the given x, y point.
* @param position the Coord to find the containing region for; should be within bounds of the map and non-null
* @return the GreasedRegion containing the given Coord, or null if position is invalid or null
*/
public GreasedRegion positionToContaining(Coord position)
{
if(position == null)
return null;
return numberToRegion(positionToNumber(position));
}
/**
* Gets the list of connected sections (by their identifying numbers) given an identifying number of a section.
* @param number an identifying number; should be non-negative and less than {@link #size()}
* @return an IntVLA storing the identifying numbers of connected sections, or null if given an invalid parameter
*/
public IntVLA numberToConnections(int number)
{
if(number < 0 || number >= connections.size())
return null;
return connections.get(number);
}
/**
* Gets the list of connected sections (by their identifying numbers) given a name of a section.
* @param name a String name; should be present in this SectionMap or this will return null
* @return an IntVLA storing the identifying numbers of connected sections, or null if given an invalid parameter
*/
public IntVLA nameToConnections(String name)
{
return numberToConnections(nameToNumber(name));
}
/**
* Gets the list of connected sections (by their identifying numbers) given a position inside that section.
* @param x the x-coordinate of the position to look up; should be within bounds
* @param y the y-coordinate of the position to look up; should be within bounds
* @return an IntVLA storing the identifying numbers of connected sections, or null if given invalid parameters
*/
public IntVLA positionToConnections(int x, int y)
{
return numberToConnections(positionToNumber(x, y));
}
/**
* Gets the list of connected sections (by their identifying numbers) given a position inside that section.
* @param position the Coord position to look up; should be within bounds and non-null
* @return an IntVLA storing the identifying numbers of connected sections, or null if given an invalid parameter
*/
public IntVLA positionToConnections(Coord position)
{
return numberToConnections(positionToNumber(position));
}
/**
* The number of regions this knows about; includes an entry for "unused cells" so this may be one larger than the
* amount of GreasedRegions present in a RoomFinder used to construct this.
* @return the size of this SectionMap
*/
public int size()
{
return names.size();
}
/**
* Checks if this contains the given name.
* @param name the name to check
* @return true if this contains the name, false otherwise
*/
public boolean contains(String name)
{
return names.containsKey(name);
}
/**
* Checks if this contains the given identifying number.
* @param number the number to check
* @return true if this contains the identifying number, false otherwise
*/
public boolean contains(int number)
{
return number >= 0 && number < names.size();
}
/**
* Checks if this contains the given position (that is, x and y are within map bounds).
* @param x the x-coordinate of the position to check
* @param y the y-coordinate of the position to check
* @return true if the given position is in bounds, false otherwise
*/
public boolean contains(int x, int y)
{
return x >= 0 && x < map.length && y >= 0 && y < map[x].length;
}
/**
* Checks if this contains the given position (that is, it is within map bounds).
* @param position the position to check
* @return true if position is non-null and is in bounds, false otherwise
*/
public boolean contains(Coord position)
{
return position != null && contains(position.x, position.y);
}
}