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