001package squidpony.squidgrid.mapping;
002
003import squidpony.ArrayTools;
004import squidpony.squidmath.Arrangement;
005import squidpony.squidmath.Coord;
006import squidpony.squidmath.GreasedRegion;
007import squidpony.squidmath.IntVLA;
008
009import java.io.Serializable;
010import java.util.ArrayList;
011import java.util.List;
012
013/**
014 * A Map-like collection that allows storing subdivisions of a 2D array with names (always Strings) and
015 * identifying numbers, then looking up {@link Coord}s to find the associated name and/or number, or or looking up
016 * a subdivision with a name or number to get a {@link GreasedRegion} back. This also stores connections between
017 * sections, which can be useful as part of a graph-like algorithm. It is fed the information it needs by a
018 * {@link RoomFinder} instance passed to the constructor or to {@link #reinitialize(RoomFinder)}. A RoomFinder is ready
019 * for usage after generating any dungeon with {@link SectionDungeonGenerator} or one of its subclasses; the field
020 * {@link SectionDungeonGenerator#finder} should usually be all that needs to be given to this class. If you don't use
021 * SectionDungeonGenerator, there's no reason you can't construct a RoomFinder independently and pass that (they can
022 * take a little time to construct on large or very complex maps, but shouldn't be heavy after construction).
023 * <br>
024 * If your code uses the {@link squidpony.squidgrid.zone.Zone} interface, then the {@link GreasedRegion} objects this
025 * can return do implement Zone, {@link squidpony.squidgrid.zone.MutableZone}, and {@link Iterable} of Coord.
026 * GreasedRegion is significantly faster than the alternatives ({@link squidpony.squidmath.CoordPacker} and manual
027 * Lists of Coord) for the spatial manipulations that RoomFinder needs to do to find room-like shapes, and this just
028 * gets its GreasedRegion values from RoomFinder directly.
029 * <br>
030 * Not to be confused with {@link squidpony.squidmath.RegionMap}, which has different functionality and a different
031 * pupose; RegionMap simply is a slight extension on OrderedMap to conveniently handle regions as short arrays produced
032 * by {@link squidpony.squidmath.CoordPacker}, while this class offers additional features for labeling and looking up
033 * sections of a map that were found by a {@link RoomFinder}.
034 * <br>
035 * Created by Tommy Ettinger on 11/28/2016.
036 */
037public class SectionMap implements Serializable {
038    private static final long serialVersionUID = -2322572367863327331L;
039
040    protected int[][] map;
041    protected Arrangement<String> names;
042    protected ArrayList<GreasedRegion> regions;
043    protected ArrayList<IntVLA> connections;
044
045    /**
046     * This shouldn't usually be used unless you for some reason need to construct a SectionMap before you have access
047     * to a dungeon for it to map. If you do need this, then you must call {@link #reinitialize(RoomFinder)} to get any
048     * use out of this object.
049     * @see #SectionMap(RoomFinder) The preferred constructor, which takes a RoomFinder.
050     */
051    public SectionMap()
052    {
053        map = new int[0][0];
054        names = new Arrangement<>(0);
055        regions = new ArrayList<>(0);
056        connections = new ArrayList<>(0);
057    }
058
059    /**
060     * The preferred constructor; takes a RoomFinder (often one already created in dungeon generation and available via
061     * {@link SectionDungeonGenerator#finder}) and uses it to give unique String names and identifying numbers to each
062     * room, corridor, and cave area that had been identified by that RoomFinder. In the rare but possible chance that
063     * a room, corridor, or cave overlaps with another such area, the one given the highest identifying number takes
064     * precedence, but this should probably only happen if RoomFinder was subclassed or its internal state was modified.
065     * Any cells that aren't a room, corridor, or cave (usually this contains all walls) are given identifying number 0,
066     * with the corresponding name "unused0." All other cells will then have positive, non-zero identifying numbers.
067     * Rooms are named next, starting at "room1" and going up to "room2" and so on until all rooms are named; the 1 in
068     * the name corresponds to the identifying number. After the last room has been found, e.g. "room5", then corridors
069     * are named, starting after the last room's number, so in the example that would be "corridor6", followed by
070     * "corridor7". The numbers in the names still correspond to identifying numbers. After corridors, caves follow the
071     * same pattern; in this example "cave8" would be followed by "cave9".
072     * @param rf a RoomFinder object; usually obtained via {@link SectionDungeonGenerator#finder}
073     */
074    public SectionMap(RoomFinder rf)
075    {
076        if(rf == null)
077        {
078            map = new int[0][0];
079            names = new Arrangement<>(0);
080            regions = new ArrayList<>(0);
081            connections = new ArrayList<>(0);
082            return;
083        }
084        regions = new ArrayList<>(rf.rooms.size() + rf.caves.size() + rf.corridors.size());
085        names = new Arrangement<>(0);
086        connections = new ArrayList<>(regions.size());
087        reinitialize(rf);
088    }
089
090    /**
091     * Copy constructor; takes an already-initialized SectionMap and deep-copies each element into this one. Allows null
092     * for {@code other}, which will make an empty SectionMap. This shouldn't be needed very often because SectionMap
093     * values are immutable, though their contents can in some cases be mutated independently, and this would allow one
094     * SectionMap to be copied and then have its items changed without changing the original.
095     * @param other a SectionMap to deep-copy into this one
096     */
097    public SectionMap(SectionMap other)
098    {
099        if(other == null) {
100            map = new int[0][0];
101            names = new Arrangement<>(0);
102            regions = new ArrayList<>(0);
103            connections = new ArrayList<>(0);
104            return;
105        }
106        map = ArrayTools.copy(other.map);
107        names = new Arrangement<>(other.names);
108        regions = new ArrayList<>(other.regions.size());
109        connections = new ArrayList<>(other.connections.size());
110        for (int i = 0; i < other.connections.size(); i++) {
111            regions.add(new GreasedRegion(other.regions.get(i)));
112            connections.add(new IntVLA(other.connections.get(i)));
113        }
114    }
115    /**
116     * If this SectionMap hasn't been initialized or the map has completely changed (such as if the player went to a
117     * different floor of a dungeon), then you can call this method to avoid discarding some of the state from an
118     * earlier SectionMap. This does all the same steps {@link #SectionMap(RoomFinder)} does, so refer to that
119     * constructor's documentation for the names and numbers this assigns.
120     * @param rf a RoomFinder object; usually obtained via {@link SectionDungeonGenerator#finder}
121     * @return this for chaining.
122     */
123    public SectionMap reinitialize(RoomFinder rf)
124    {
125        if(rf == null)
126        {
127            map = new int[0][0];
128            names = new Arrangement<>(0);
129            regions = new ArrayList<>(0);
130            connections = new ArrayList<>(0);
131            return this;
132        }
133        map = new int[rf.width][rf.height];
134        regions.clear();
135        names.clear();
136        connections.clear();
137        GreasedRegion t, all = new GreasedRegion(map, 0);
138        regions.add(all);
139        names.add("unused0");
140        connections.add(new IntVLA(0));
141        for (int i = 0; i < rf.rooms.size(); i++) {
142            t = rf.rooms.keyAt(i);
143            regions.add(t);
144            all.andNot(t);
145            t.writeIntsInto(map, names.size());
146            names.add("room"+names.size());
147            connections.add(new IntVLA(rf.rooms.getAt(i).size()));
148        }
149        for (int i = 0; i < rf.corridors.size(); i++) {
150            t = rf.corridors.keyAt(i);
151            regions.add(t);
152            all.andNot(t);
153            t.writeIntsInto(map, names.size());
154            names.add("corridor"+names.size());
155            connections.add(new IntVLA(rf.corridors.getAt(i).size()));
156        }
157        for (int i = 0; i < rf.caves.size(); i++) {
158            t = rf.caves.keyAt(i);
159            regions.add(t);
160            all.andNot(t);
161            t.writeIntsInto(map, names.size());
162            names.add("cave"+names.size());
163            connections.add(new IntVLA(rf.caves.getAt(i).size()));
164        }
165        int ls = 1;
166        List<GreasedRegion> connected;
167        IntVLA iv;
168        for (int i = 0; i < rf.rooms.size(); i++, ls++) {
169            connected = rf.rooms.getAt(i);
170            iv = connections.get(ls);
171            for (int j = 0; j < connected.size(); j++) {
172                iv.add(positionToNumber(connected.get(j).first()));
173            }
174        }
175        for (int i = 0; i < rf.corridors.size(); i++, ls++) {
176            connected = rf.corridors.getAt(i);
177            iv = connections.get(ls);
178            for (int j = 0; j < connected.size(); j++) {
179                iv.add(positionToNumber(connected.get(j).first()));
180            }
181        }
182        for (int i = 0; i < rf.caves.size(); i++, ls++) {
183            connected = rf.caves.getAt(i);
184            iv = connections.get(ls);
185            for (int j = 0; j < connected.size(); j++) {
186                iv.add(positionToNumber(connected.get(j).first()));
187            }
188        }
189        return this;
190    }
191
192    /**
193     * Gets the identifying number of the area that contains the given x, y position.
194     * @param x the x-coordinate to find the identifying number for; should be within bounds of the map
195     * @param y the y-coordinate to find the identifying number for; should be within bounds of the map
196     * @return the corresponding identifying number, or -1 if the parameters are invalid
197     */
198    public int positionToNumber(int x, int y)
199    {
200        if(x < 0 || y < 0 || x >= map.length || y >= map[x].length)
201            return -1;
202        return map[x][y];
203    }
204
205    /**
206     * Gets the identifying number of the area that contains the given position.
207     * @param position the Coord to find the identifying number for; should be within bounds of the map and non-null
208     * @return the corresponding identifying number, or -1 if position is invalid or null
209     */
210    public int positionToNumber(Coord position)
211    {
212        if(position == null)
213            return -1;
214        return positionToNumber(position.x, position.y);
215    }
216
217    /**
218     * Gets the name of the area that contains the given x, y position.
219     * @param x the x-coordinate to find the name for; should be within bounds of the map
220     * @param y the y-coordinate to find the name for; should be within bounds of the map
221     * @return the corresponding name as a String, or null if the parameters are invalid
222     */
223    public String positionToName(int x, int y)
224    {
225        return numberToName(positionToNumber(x, y));
226    }
227
228    /**
229     * Gets the name of the area that contains the given position.
230     * @param position a Coord that should be within bounds of the map and non-null
231     * @return the corresponding name as a String, or null if position is invalid or null
232     */
233    public String positionToName(Coord position)
234    {
235        if(position == null)
236            return null;
237        return numberToName(positionToNumber(position));
238    }
239
240    /**
241     * Gets the identifying number corresponding to the given name.
242     * @param name the name to look up, like "room1"
243     * @return the corresponding identifying number, or -1 if no such name exists
244     */
245    public int nameToNumber(String name)
246    {
247        return names.getInt(name);
248    }
249
250    /**
251     * Gets the name that corresponds to the given identifying number.
252     * @param number the number to look up, like 1
253     * @return the corresponding name as a String, or null if no such number is used
254     */
255    public String numberToName(int number)
256    {
257        return names.keyAt(number);
258    }
259
260    /**
261     * Gets the GreasedRegion that has the given identifying number.
262     * @param number the number to look up, like 1
263     * @return the corresponding GreasedRegion, or null if no such number is used
264     */
265    public GreasedRegion numberToRegion(int number)
266    {
267        if(number < 0 || number >= regions.size())
268            return null;
269        return regions.get(number);
270    }
271
272    /**
273     * Gets the GreasedRegion that has the given name.
274     * @param name the name to look up, like "room1"
275     * @return the corresponding GreasedRegion, or null if no such name exists
276     */
277    public GreasedRegion nameToRegion(String name)
278    {
279        return numberToRegion(nameToNumber(name));
280    }
281
282    /**
283     * Gets the GreasedRegion (a group of points as made by the constructor) that contains the given x, y point.
284     * @param x the x-coordinate to find the containing region for; should be within bounds of the map
285     * @param y the y-coordinate to find the containing region for; should be within bounds of the map
286     * @return the GreasedRegion containing the given point, or null if the parameters are invalid
287     */
288    public GreasedRegion positionToContaining(int x, int y)
289    {
290        return numberToRegion(positionToNumber(x, y));
291    }
292    /**
293     * Gets the GreasedRegion (a group of points as made by the constructor) that contains the given x, y point.
294     * @param position the Coord to find the containing region for; should be within bounds of the map and non-null
295     * @return the GreasedRegion containing the given Coord, or null if position is invalid or null
296     */
297    public GreasedRegion positionToContaining(Coord position)
298    {
299        if(position == null)
300            return null;
301        return numberToRegion(positionToNumber(position));
302    }
303
304    /**
305     * Gets the list of connected sections (by their identifying numbers) given an identifying number of a section.
306     * @param number an identifying number; should be non-negative and less than {@link #size()}
307     * @return an IntVLA storing the identifying numbers of connected sections, or null if given an invalid parameter
308     */
309    public IntVLA numberToConnections(int number)
310    {
311        if(number < 0 || number >= connections.size())
312            return null;
313        return connections.get(number);
314    }
315    /**
316     * Gets the list of connected sections (by their identifying numbers) given a name of a section.
317     * @param name a String name; should be present in this SectionMap or this will return null
318     * @return an IntVLA storing the identifying numbers of connected sections, or null if given an invalid parameter
319     */
320    public IntVLA nameToConnections(String name)
321    {
322        return numberToConnections(nameToNumber(name));
323    }
324
325    /**
326     * Gets the list of connected sections (by their identifying numbers) given a position inside that section.
327     * @param x the x-coordinate of the position to look up; should be within bounds
328     * @param y the y-coordinate of the position to look up; should be within bounds
329     * @return an IntVLA storing the identifying numbers of connected sections, or null if given invalid parameters
330     */
331    public IntVLA positionToConnections(int x, int y)
332    {
333        return numberToConnections(positionToNumber(x, y));
334    }
335
336    /**
337     * Gets the list of connected sections (by their identifying numbers) given a position inside that section.
338     * @param position the Coord position to look up; should be within bounds and non-null
339     * @return an IntVLA storing the identifying numbers of connected sections, or null if given an invalid parameter
340     */
341    public IntVLA positionToConnections(Coord position)
342    {
343        return numberToConnections(positionToNumber(position));
344    }
345
346    /**
347     * The number of regions this knows about; includes an entry for "unused cells" so this may be one larger than the
348     * amount of GreasedRegions present in a RoomFinder used to construct this.
349     * @return the size of this SectionMap
350     */
351    public int size()
352    {
353        return names.size();
354    }
355
356    /**
357     * Checks if this contains the given name.
358     * @param name the name to check
359     * @return true if this contains the name, false otherwise
360     */
361    public boolean contains(String name)
362    {
363        return names.containsKey(name);
364    }
365
366    /**
367     * Checks if this contains the given identifying number.
368     * @param number the number to check
369     * @return true if this contains the identifying number, false otherwise
370     */
371    public boolean contains(int number)
372    {
373        return number >= 0 && number < names.size();
374    }
375
376    /**
377     * Checks if this contains the given position (that is, x and y are within map bounds).
378     * @param x the x-coordinate of the position to check
379     * @param y the y-coordinate of the position to check
380     * @return true if the given position is in bounds, false otherwise
381     */
382    public boolean contains(int x, int y)
383    {
384        return x >= 0 && x < map.length && y >= 0 && y < map[x].length;
385    }
386    /**
387     * Checks if this contains the given position (that is, it is within map bounds).
388     * @param position the position to check
389     * @return true if position is non-null and is in bounds, false otherwise
390     */
391    public boolean contains(Coord position)
392    {
393        return position != null && contains(position.x, position.y);
394    }
395}