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}