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}