001package squidpony.squidgrid; 002 003import squidpony.squidmath.Coord; 004import squidpony.squidmath.LightRNG; 005import squidpony.squidmath.RNG; 006 007import java.util.HashMap; 008import java.util.Map; 009import java.util.Set; 010 011/** 012 * This class is used to determine when a sound is audible on a map and at what positions. 013 * Created by Tommy Ettinger on 4/4/2015. 014 */ 015public class SoundMap 016{ 017 /** 018 * The type of heuristic to use. Note that EUCLIDEAN is not an option here because it would only affect paths, and 019 * there is no path-finding functionality in this class. 020 */ 021 public enum Measurement { 022 023 /** 024 * The distance it takes when only the four primary directions can be 025 * moved in. The default. 026 */ 027 MANHATTAN, 028 /** 029 * The distance it takes when diagonal movement costs the same as 030 * cardinal movement. 031 */ 032 CHEBYSHEV 033 } 034 035 /** 036 * This affects how sound travels on diagonal directions vs. orthogonal directions. MANHATTAN should form a diamond 037 * shape on a featureless map, while CHEBYSHEV will form a square. 038 */ 039 public Measurement measurement = Measurement.MANHATTAN; 040 041 042 /** 043 * Stores which parts of the map are accessible and which are not. Should not be changed unless the actual physical 044 * terrain has changed. You should call initialize() with a new map instead of changing this directly. 045 */ 046 public double[][] physicalMap; 047 /** 048 * The frequently-changing values that are often the point of using this class; cells producing sound will have a 049 * value greater than 0, cells that cannot possibly be reached by a sound will have a value of exactly 0, and walls 050 * will have a value equal to the WALL constant (a negative number). 051 */ 052 public double[][] gradientMap; 053 /** 054 * Height of the map. Exciting stuff. Don't change this, instead call initialize(). 055 */ 056 public int height; 057 /** 058 * Width of the map. Exciting stuff. Don't change this, instead call initialize(). 059 */ 060 public int width; 061 /** 062 * The latest results of findAlerted(), with Coord keys representing the positions of creatures that were alerted 063 * and Double values representing how loud the sound was when it reached them. 064 */ 065 public HashMap<Coord, Double> alerted = new HashMap<>(); 066 /** 067 * Cells with no sound are always marked with 0. 068 */ 069 public static final double SILENT = 0.0; 070 /** 071 * Walls, which are solid no-entry cells, are marked with a significant negative number equal to -999500.0 . 072 */ 073 public static final double WALL = -999500.0; 074 /** 075 * Sources of sound on the map; keys are positions, values are how loud the noise is (10.0 should spread 10 cells 076 * away, with diminishing values assigned to further positions). 077 */ 078 public HashMap<Coord, Double> sounds; 079 private HashMap<Coord, Double> fresh; 080 /** 081 * The RNG used to decide which one of multiple equally-short paths to take. 082 */ 083 public RNG rng; 084 085 private boolean initialized = false; 086 /** 087 * Construct a SoundMap without a level to actually scan. If you use this constructor, you must call an 088 * initialize() method before using this class. 089 */ 090 public SoundMap() { 091 rng = new RNG(new LightRNG()); 092 alerted = new HashMap<>(); 093 fresh = new HashMap<>(); 094 sounds = new HashMap<>(); 095 } 096 097 /** 098 * Construct a SoundMap without a level to actually scan. This constructor allows you to specify an RNG before it is 099 * used. If you use this constructor, you must call an initialize() method before using this class. 100 */ 101 public SoundMap(RNG random) { 102 rng = random; 103 alerted = new HashMap<>(); 104 fresh = new HashMap<>(); 105 sounds = new HashMap<>(); 106 } 107 108 /** 109 * Used to construct a SoundMap from the output of another. Any sounds will need to be assigned again. 110 * @param level 111 */ 112 public SoundMap(final double[][] level) { 113 rng = new RNG(new LightRNG()); 114 alerted = new HashMap<>(); 115 fresh = new HashMap<>(); 116 sounds = new HashMap<>(); 117 initialize(level); 118 } 119 /** 120 * Used to construct a DijkstraMap from the output of another, specifying a distance calculation. 121 * @param level 122 * @param measurement 123 */ 124 public SoundMap(final double[][] level, Measurement measurement) { 125 rng = new RNG(new LightRNG()); 126 this.measurement = measurement; 127 alerted = new HashMap<>(); 128 fresh = new HashMap<>(); 129 sounds = new HashMap<>(); 130 initialize(level); 131 } 132 133 /** 134 * Constructor meant to take a char[][] returned by DungeonGen.generate(), or any other 135 * char[][] where '#' means a wall and anything else is a walkable tile. If you only have 136 * a map that uses box-drawing characters, use DungeonUtility.linesToHashes() to get a 137 * map that can be used here. 138 * 139 * @param level 140 */ 141 public SoundMap(final char[][] level) { 142 rng = new RNG(new LightRNG()); 143 alerted = new HashMap<>(); 144 fresh = new HashMap<>(); 145 sounds = new HashMap<>(); 146 initialize(level); 147 } 148 /** 149 * Constructor meant to take a char[][] returned by DungeonGen.generate(), or any other 150 * char[][] where one char means a wall and anything else is a walkable tile. If you only have 151 * a map that uses box-drawing characters, use DungeonUtility.linesToHashes() to get a 152 * map that can be used here. You can specify the character used for walls. 153 * 154 * @param level 155 */ 156 public SoundMap(final char[][] level, char alternateWall) { 157 rng = new RNG(new LightRNG()); 158 alerted = new HashMap<>(); 159 fresh = new HashMap<>(); 160 sounds = new HashMap<>(); 161 initialize(level, alternateWall); 162 } 163 164 /** 165 * Constructor meant to take a char[][] returned by DungeonGen.generate(), or any other 166 * char[][] where '#' means a wall and anything else is a walkable tile. If you only have 167 * a map that uses box-drawing characters, use DungeonUtility.linesToHashes() to get a 168 * map that can be used here. This constructor specifies a distance measurement. 169 * 170 * @param level 171 * @param measurement 172 */ 173 public SoundMap(final char[][] level, Measurement measurement) { 174 rng = new RNG(new LightRNG()); 175 this.measurement = measurement; 176 alerted = new HashMap<>(); 177 fresh = new HashMap<>(); 178 sounds = new HashMap<>(); 179 initialize(level); 180 } 181 182 /** 183 * Used to initialize or re-initialize a SoundMap that needs a new PhysicalMap because it either wasn't given 184 * one when it was constructed, or because the contents of the terrain have changed permanently. 185 * @param level 186 * @return 187 */ 188 public SoundMap initialize(final double[][] level) { 189 width = level.length; 190 height = level[0].length; 191 gradientMap = new double[width][height]; 192 physicalMap = new double[width][height]; 193 for (int y = 0; y < height; y++) { 194 for (int x = 0; x < width; x++) { 195 gradientMap[x][y] = level[x][y]; 196 physicalMap[x][y] = level[x][y]; 197 } 198 } 199 initialized = true; 200 return this; 201 } 202 203 /** 204 * Used to initialize or re-initialize a SoundMap that needs a new PhysicalMap because it either wasn't given 205 * one when it was constructed, or because the contents of the terrain have changed permanently. 206 * @param level 207 * @return 208 */ 209 public SoundMap initialize(final char[][] level) { 210 width = level.length; 211 height = level[0].length; 212 gradientMap = new double[width][height]; 213 physicalMap = new double[width][height]; 214 for (int y = 0; y < height; y++) { 215 for (int x = 0; x < width; x++) { 216 double t = (level[x][y] == '#') ? WALL : SILENT; 217 gradientMap[x][y] = t; 218 physicalMap[x][y] = t; 219 } 220 } 221 initialized = true; 222 return this; 223 } 224 225 /** 226 * Used to initialize or re-initialize a SoundMap that needs a new PhysicalMap because it either wasn't given 227 * one when it was constructed, or because the contents of the terrain have changed permanently. This 228 * initialize() method allows you to specify an alternate wall char other than the default character, '#' . 229 * @param level 230 * @param alternateWall 231 * @return 232 */ 233 public SoundMap initialize(final char[][] level, char alternateWall) { 234 width = level.length; 235 height = level[0].length; 236 gradientMap = new double[width][height]; 237 physicalMap = new double[width][height]; 238 for (int y = 0; y < height; y++) { 239 for (int x = 0; x < width; x++) { 240 double t = (level[x][y] == alternateWall) ? WALL : SILENT; 241 gradientMap[x][y] = t; 242 physicalMap[x][y] = t; 243 } 244 } 245 initialized = true; 246 return this; 247 } 248 249 /** 250 * Resets the gradientMap to its original value from physicalMap. Does not remove sounds (they will still affect 251 * scan() normally). 252 */ 253 public void resetMap() { 254 if(!initialized) return; 255 for (int y = 0; y < height; y++) { 256 for (int x = 0; x < width; x++) { 257 gradientMap[x][y] = physicalMap[x][y]; 258 } 259 } 260 } 261 262 /** 263 * Resets this SoundMap to a state with no sounds, no alerted creatures, and no changes made to gradientMap 264 * relative to physicalMap. 265 */ 266 public void reset() { 267 resetMap(); 268 alerted.clear(); 269 fresh.clear(); 270 sounds.clear(); 271 } 272 273 /** 274 * Marks a cell as producing a sound with the given loudness; this can be placed on a wall or unreachable area, 275 * but that may cause the sound to be un-hear-able. A sound emanating from a cell on one side of a 2-cell-thick 276 * wall will only radiate sound on one side, which can be used for certain effects. A sound emanating from a cell 277 * in a 1-cell-thick wall will radiate on both sides. 278 * @param x 279 * @param y 280 * @param loudness The number of cells the sound should spread away using the current measurement. 281 */ 282 public void setSound(int x, int y, double loudness) { 283 if(!initialized) return; 284 Coord pt = Coord.get(x, y); 285 if(sounds.containsKey(pt) && sounds.get(pt) >= loudness) 286 return; 287 sounds.put(pt, loudness); 288 } 289 290 /** 291 * Marks a cell as producing a sound with the given loudness; this can be placed on a wall or unreachable area, 292 * but that may cause the sound to be un-hear-able. A sound emanating from a cell on one side of a 2-cell-thick 293 * wall will only radiate sound on one side, which can be used for certain effects. A sound emanating from a cell 294 * in a 1-cell-thick wall will radiate on both sides. 295 * @param pt 296 * @param loudness The number of cells the sound should spread away using the current measurement. 297 */ 298 public void setSound(Coord pt, double loudness) { 299 if(!initialized) return; 300 if(sounds.containsKey(pt) && sounds.get(pt) >= loudness) 301 return; 302 sounds.put(pt, loudness); 303 } 304 305 /** 306 * If a sound is being produced at a given (x, y) location, this removes it. 307 * @param x 308 * @param y 309 */ 310 public void removeSound(int x, int y) { 311 if(!initialized) return; 312 Coord pt = Coord.get(x, y); 313 if(sounds.containsKey(pt)) 314 sounds.remove(pt); 315 } 316 317 /** 318 * If a sound is being produced at a given location (a Coord), this removes it. 319 * @param pt 320 */ 321 public void removeSound(Coord pt) { 322 if(!initialized) return; 323 if(sounds.containsKey(pt)) 324 sounds.remove(pt); 325 } 326 327 /** 328 * Marks a specific cell in gradientMap as a wall, which makes sounds potentially unable to pass through it. 329 * @param x 330 * @param y 331 */ 332 public void setOccupied(int x, int y) { 333 if(!initialized) return; 334 gradientMap[x][y] = WALL; 335 } 336 337 /** 338 * Reverts a cell to the value stored in the original state of the level as known by physicalMap. 339 * @param x 340 * @param y 341 */ 342 public void resetCell(int x, int y) { 343 if(!initialized) return; 344 gradientMap[x][y] = physicalMap[x][y]; 345 } 346 347 /** 348 * Reverts a cell to the value stored in the original state of the level as known by physicalMap. 349 * @param pt 350 */ 351 public void resetCell(Coord pt) { 352 if(!initialized) return; 353 gradientMap[pt.x][pt.y] = physicalMap[pt.x][pt.y]; 354 } 355 356 /** 357 * Used to remove all sounds. 358 */ 359 public void clearSounds() { 360 if(!initialized) return; 361 sounds.clear(); 362 } 363 364 protected void setFresh(int x, int y, double counter) { 365 if(!initialized) return; 366 gradientMap[x][y] = counter; 367 fresh.put(Coord.get(x, y), counter); 368 } 369 370 protected void setFresh(final Coord pt, double counter) { 371 if(!initialized) return; 372 gradientMap[pt.x][pt.y] = counter; 373 fresh.put(Coord.get(pt.x, pt.y), counter); 374 } 375 376 /** 377 * Recalculate the sound map and return it. Cells that were marked as goals with setSound will have 378 * a value greater than 0 (higher numbers are louder sounds), the cells adjacent to sounds will have a value 1 less 379 * than the loudest adjacent cell, and cells progressively further from sounds will have a value equal to the 380 * loudness of the nearest sound minus the distance from it, to a minimum of 0. The exceptions are walls, 381 * which will have a value defined by the WALL constant in this class. Like sound itself, the sound map 382 * allows some passage through walls; specifically, 1 cell thick of wall can be passed through, with reduced 383 * loudness, before the fill cannot go further. This uses the current measurement. 384 * 385 * @return A 2D double[width][height] using the width and height of what this knows about the physical map. 386 */ 387 public double[][] scan() { 388 if(!initialized) return null; 389 390 for (Map.Entry<Coord, Double> entry : sounds.entrySet()) { 391 gradientMap[entry.getKey().x][entry.getKey().y] = entry.getValue(); 392 if(fresh.containsKey(entry.getKey()) && fresh.get(entry.getKey()) > entry.getValue()) 393 { 394 } 395 else 396 { 397 fresh.put(entry.getKey(), entry.getValue()); 398 } 399 400 } 401 int numAssigned = fresh.size(); 402 403 Direction[] dirs = (measurement == Measurement.MANHATTAN) ? Direction.CARDINALS : Direction.OUTWARDS; 404 405 while (numAssigned > 0) { 406 numAssigned = 0; 407 HashMap<Coord, Double> fresh2 = new HashMap<>(fresh.size()); 408 fresh2.putAll(fresh); 409 fresh.clear(); 410 411 for (Map.Entry<Coord, Double> cell : fresh2.entrySet()) { 412 if(cell.getValue() <= 1) //We shouldn't assign values lower than 1. 413 continue; 414 for (int d = 0; d < dirs.length; d++) { 415 Coord adj = cell.getKey().translate(dirs[d].deltaX, dirs[d].deltaY); 416 if(adj.x < 0 || adj.x >= width || adj.y < 0 || adj.y >= height) 417 continue; 418 if(physicalMap[cell.getKey().x][cell.getKey().y] == WALL && physicalMap[adj.x][adj.y] == WALL) 419 continue; 420 if (gradientMap[cell.getKey().x][cell.getKey().y] > gradientMap[adj.x][adj.y] + 1) { 421 double v = cell.getValue() - 1 - ((physicalMap[adj.x][adj.y] == WALL) ? 1 : 0); 422 if (v > 0) { 423 gradientMap[adj.x][adj.y] = v; 424 fresh.put(Coord.get(adj.x, adj.y), v); 425 ++numAssigned; 426 } 427 } 428 } 429 } 430 } 431 432 for (int y = 0; y < height; y++) { 433 for (int x = 0; x < width; x++) { 434 if (physicalMap[x][y] == WALL) { 435 gradientMap[x][y] = WALL; 436 } 437 } 438 } 439 440 return gradientMap; 441 } 442 443 /** 444 * Scans the dungeon using SoundMap.scan, adding any positions in extraSounds to the group of known sounds before 445 * scanning. The creatures passed to this function as a Set of Points will have the loudness of all sounds at 446 * their position put as the value in alerted corresponding to their Coord position. 447 * 448 * @param creatures 449 * @param extraSounds 450 * @return 451 */ 452 public HashMap<Coord, Double> findAlerted(Set<Coord> creatures, Map<Coord, Double> extraSounds) { 453 if(!initialized) return null; 454 alerted = new HashMap<>(creatures.size()); 455 456 resetMap(); 457 for (Map.Entry<Coord, Double> sound : extraSounds.entrySet()) { 458 setSound(sound.getKey(), sound.getValue()); 459 } 460 scan(); 461 for(Coord critter : creatures) 462 { 463 if(critter.x < 0 || critter.x >= width || critter.y < 0 || critter.y >= height) 464 continue; 465 alerted.put(Coord.get(critter.x, critter.y), gradientMap[critter.x][critter.y]); 466 } 467 return alerted; 468 } 469}