001package squidpony.squidgrid.mapping;
002
003import squidpony.ArrayTools;
004import squidpony.FakeLanguageGen;
005import squidpony.Thesaurus;
006import squidpony.annotation.Beta;
007import squidpony.squidgrid.Direction;
008import squidpony.squidmath.BlueNoise;
009import squidpony.squidmath.IRNG;
010import squidpony.squidmath.IStatefulRNG;
011import squidpony.squidmath.SilkRNG;
012
013import java.io.Serializable;
014import java.util.ArrayList;
015
016import static squidpony.Maker.makeList;
017
018/**
019 * A finite 2D area map for some kind of wilderness, adapting to different ecosystems by changing its output.
020 * Regional maps for wilderness areas have very different requirements from mostly-artificial dungeons. This is intended
021 * to work alongside {@link WorldMapGenerator} and {@link WorldMapGenerator.DetailedBiomeMapper} to produce, for
022 * example, very sparse maps with an occasional cactus in a desert, or very dense maps with many trees and shrubs for a
023 * forest.
024 * <br>
025 * Using this code mostly involves constructing a WildMap with a width, height, biome, and optionally a random number
026 * generator, an ArrayList of floor types (as Strings) that can appear, and an ArrayList of terrain content that can
027 * appear (also as Strings). Then you can call {@link #generate()}, which assigns indices into {@link #content} and
028 * {@link #floors}, where an index can look up a value from {@link #contentTypes} or {@link #floorTypes}. The biome is
029 * usually an index into {@link squidpony.squidgrid.mapping.WorldMapGenerator.DetailedBiomeMapper#biomeTable}, but can
030 * be some other index if you don't use DetailedBiomeMapper (you would probably use a subclass then). The
031 * {@link #contentTypes} field is an ArrayList; you can have and are encouraged to have duplicates when an object should
032 * appear more often. An index of -1 in content indicates nothing of note is present there. There is also a String array
033 * of {@link #floorTypes} that is not typically user-set unless you subclass WildMap yourself; it is used to look up the
034 * indices in {@link #floors}. The floors are set to reasonable values for the particular biome, so a forest has "dirt"
035 * and "leaves" among others, while a desert might only have "sand". Again, only the indices matter, so you could change
036 * the values in {@link #floorTypes} to match names of textures in a graphical game and make lookup easier, or to a char
037 * followed by the name of a color (as in SColor in the display module) for a text-based game.
038 * <br>
039 * This is marked Beta because there's still some work to be done, and the actual output will change even if the API
040 * doesn't have any breaks. While the wilderness maps this produces are usable, they don't have paths or areas that a
041 * character would have to find a way around (like a cliff). This is meant to be added at some point, probably in
042 * conjunction with some system for connecting WildMaps.
043 * <br>
044 * Created by Tommy Ettinger on 10/16/2019.
045 */
046@Beta
047public class WildMap implements Serializable {
048    private static final long serialVersionUID = 1L;
049    public final int width, height;
050    public int biome;
051    public IStatefulRNG rng;
052    public ArrayList<String> contentTypes;
053    public ArrayList<String> floorTypes;
054    public final int[][] content, floors;
055
056    /**
057     * Meant for generating large ArrayLists of Strings where an individual String may occur quite a few times.
058     * The rest parameter is a vararg (it may also be an Object array) of alternating String and Integer values, where
059     * an Integer is how many times to repeat the preceding String in the returned ArrayList.
060     * @param rest a vararg (or Object array) of alternating String and Integer values
061     * @return an ArrayList of Strings, probably with some or most of them repeated; you may want to shuffle this result
062     */
063    public static ArrayList<String> makeRepeats(Object... rest)
064    {
065        if(rest == null || rest.length < 2)
066        {
067            return new ArrayList<>(0);
068        }
069        ArrayList<String> al = new ArrayList<>(rest.length);
070
071        for (int i = 0; i < rest.length - 1; i+=2) {
072            try {
073                int count = (int)rest[i+1];
074                String v = (String) rest[i];
075                for (int j = 0; j < count; j++) {
076                    al.add(v);
077                }
078            }catch (ClassCastException ignored) {
079            }
080        }
081        return al;
082    }
083    public static ArrayList<String> makeShuffledRepeats(IRNG rng, Object... rest) 
084    {
085        ArrayList<String> al = makeRepeats(rest);
086        rng.shuffleInPlace(al);
087        return al;
088    }
089    public static ArrayList<String> makeVegetation(IRNG rng, int size, double monoculture, FakeLanguageGen naming)
090    {
091        Thesaurus t = new Thesaurus(rng);
092        ArrayList<String> al = new ArrayList<>(size);
093        String latest;
094        for (int i = size; i > 0; i--) {
095            al.add(latest = t.makePlantName(naming));
096            for (double j = rng.nextDouble(monoculture * 2.0 * size); j >= 1 && i > 0; j--, i--) {
097                al.add(latest);
098            }
099        }
100        rng.shuffleInPlace(al);
101        return al;
102    }
103
104    /**
105     * Gets a list of Strings that are really just the names of types of floor tile for wilderness areas.
106     * @param biome an index into {@link squidpony.squidgrid.mapping.WorldMapGenerator.DetailedBiomeMapper#biomeTable}, or some other index if you don't use DetailedBiomeMapper
107     * @param rng an IRNG, like {@link squidpony.squidmath.RNG} or {@link squidpony.squidmath.GWTRNG} 
108     * @return a shuffled ArrayList that typically contains repeats of the kinds of floor that can appear here
109     */
110    public static ArrayList<String> floorsByBiome(int biome, IRNG rng) {
111        biome &= 1023;
112        switch (biome) {
113            case 0: //Ice
114            case 1:
115            case 6:
116            case 12:
117            case 18:
118            case 24:
119            case 30:
120            case 42:
121            case 48:
122                return makeShuffledRepeats(rng, "snow", 3, "ice", 1);
123            case 7: //Tundra
124            case 13:
125            case 19:
126            case 25:
127                return makeShuffledRepeats(rng, "dirt", 6, "pebbles", 1, "snow", 9, "dry grass", 4);
128            case 26: //BorealForest
129            case 31:
130            case 32:
131                return makeShuffledRepeats(rng, "dirt", 3, "pebbles", 1, "snow", 11);
132            case 43: //River
133            case 44:
134            case 45:
135            case 46:
136            case 47:
137            case 49:
138            case 50:
139            case 51:
140            case 52:
141            case 53:
142                return makeList("fresh water");
143            case 54: //Ocean
144            case 55:
145            case 56:
146            case 57:
147            case 58:
148            case 59:
149                return makeList("salt water");
150            case 3: //Desert
151            case 4:
152            case 5:
153            case 10:
154            case 11:
155            case 17:
156            case 38: //Beach
157            case 39:
158            case 40:
159            case 41:
160                return makeList("sand");
161            case 2: //Grassland
162            case 8:
163            case 9:
164                return makeShuffledRepeats(rng, "dirt", 8, "dry grass", 13, "grass", 2);
165            case 14: //Woodland
166            case 15:
167                return makeShuffledRepeats(rng, "dirt", 11, "leaves", 3, "dry grass", 8);
168            case 16: //Savanna
169            case 22:
170            case 23:
171            case 29:
172                return makeShuffledRepeats(rng, "dirt", 4, "dry grass", 17);
173            case 20: //SeasonalForest
174            case 21:
175                return makeShuffledRepeats(rng, "dirt", 9, "leaves", 6, "grass", 14);
176            case 27: //TemperateRainforest
177            case 33:
178                return makeShuffledRepeats(rng, "mud", 3, "leaves", 8, "grass", 10, "moss", 5);
179            case 28: //TropicalRainforest
180            case 34:
181            case 35:
182                return makeShuffledRepeats(rng, "mud", 7, "leaves", 6, "grass", 4, "moss", 11);
183            case 36: // Rocky
184            case 37:
185                return makeShuffledRepeats(rng, "pebbles", 5, "rubble", 1);
186            default:
187                return makeList("empty space");
188        }
189    }
190    /**
191     * Gets a list of Strings that are really just the names of types of path tile for wilderness areas.
192     * Not currently used.
193     * @param biome an index into {@link squidpony.squidgrid.mapping.WorldMapGenerator.DetailedBiomeMapper#biomeTable}, or some other index if you don't use DetailedBiomeMapper
194     * @return an ArrayList that typically contains just the one or few types of path that can appear here
195     */
196    public static ArrayList<String> pathsByBiome(int biome) {
197        biome &= 1023;
198        switch (biome) {
199            case 0: //Ice
200            case 1:
201            case 6:
202            case 12:
203            case 18:
204            case 24:
205            case 30:
206            case 42:
207            case 48:
208                return makeList("snow path");
209            case 7: //Tundra
210            case 13:
211            case 19:
212            case 25: 
213            case 26: //BorealForest
214            case 31:
215            case 32:
216                return makeList("snow path", "dirt path");
217//            case 43: //River
218//            case 44:
219//            case 45:
220//            case 46:
221//            case 47:
222//            case 49:
223//            case 50:
224//            case 51:
225//            case 52:
226//            case 53:
227//            case 54: //Ocean
228//            case 55:
229//            case 56:
230//            case 57:
231//            case 58:
232//            case 59:
233//                return makeList("wooden bridge");
234            case 3: //Desert
235            case 4:
236            case 5:
237            case 10:
238            case 11:
239            case 17:
240            case 38: //Beach
241            case 39:
242            case 40:
243            case 41:
244                return makeList("sand path");
245            case 2: //Grassland
246            case 8:
247            case 9:
248            case 14: //Woodland
249            case 15:
250            case 16: //Savanna
251            case 22:
252            case 23:
253            case 29:
254                return makeList("dirt path");
255            case 20: //SeasonalForest
256            case 21:
257                return makeList("dirt path", "grass path");
258            case 27: //TemperateRainforest
259            case 33:
260            case 28: //TropicalRainforest
261            case 34:
262            case 35:
263                return makeList("grass path");
264            case 36: // Rocky
265            case 37:
266                return makeList("stone path");
267            default:
268                return makeList("wooden bridge");
269
270        }
271    }
272    /**
273     * Gets a list of Strings that are really just the names of types of terrain feature for wilderness areas.
274     * @param biome an index into {@link squidpony.squidgrid.mapping.WorldMapGenerator.DetailedBiomeMapper#biomeTable}, or some other index if you don't use DetailedBiomeMapper
275     * @param rng an IRNG, like {@link squidpony.squidmath.RNG} or {@link squidpony.squidmath.GWTRNG}
276     * @return a shuffled ArrayList that typically contains repeats of the kinds of terrain feature that can appear here
277     */
278    public static ArrayList<String> contentByBiome(int biome, IRNG rng) {
279        biome &= 1023;
280        switch (biome) {
281            case 0: //Ice
282            case 1:
283            case 6:
284            case 12:
285            case 18:
286            case 24:
287            case 30:
288            case 42:
289            case 48:
290                return makeShuffledRepeats(rng, "snow mound", 5, "icy divot", 2, "powder snowdrift", 5);
291            case 7: //Tundra
292            case 13:
293            case 19:
294            case 25:
295                return makeShuffledRepeats(rng, "snow mound", 4, "hillock", 6, "animal burrow", 5, "small bush 1", 2);
296            case 26: //BorealForest
297            case 31:
298            case 32:
299                return makeShuffledRepeats(rng, "snow mound", 3, "small bush 1", 5, "large bush 1", 3, "evergreen tree 1", 17, "evergreen tree 2", 12);
300//                case 43: //River
301//                case 44:
302//                case 45:
303//                case 46:
304//                case 47:
305//                case 49:
306//                case 50:
307//                case 51:
308//                case 52:
309//                case 53:
310//                case 54: //Ocean
311//                case 55:
312//                case 56:
313//                case 57:
314//                case 58:
315//                case 59:
316//                    return new ArrayList<>(0);
317            case 3: //Desert
318            case 4:
319            case 5:
320            case 10:
321            case 11:
322            case 17:
323                return makeShuffledRepeats(rng, "small cactus 1", 2, "large cactus 1", 2, "succulent 1", 1, "animal burrow", 2);
324            case 38: //Beach
325            case 39:
326            case 40:
327            case 41:
328                return makeShuffledRepeats(rng, "seashell 1", 3, "seashell 2", 3, "seashell 3", 3, "seashell 4", 3, "driftwood", 5, "boulder", 3);
329            case 2: //Grassland
330            case 8:
331            case 9:
332                return makeShuffledRepeats(rng, "deciduous tree 1", 3, "small bush 1", 5, "small bush 2", 4, "large bush 1", 5, "animal burrow", 8, "hillock", 4);
333            case 14: //Woodland
334            case 15:
335                return makeShuffledRepeats(rng, "deciduous tree 1", 12, "deciduous tree 2", 9, "deciduous tree 3", 6, "small bush 1", 4, "small bush 2", 3, "animal burrow", 3);
336            case 16: //Savanna
337            case 22:
338            case 23:
339            case 29:
340                return makeShuffledRepeats(rng, "small bush 1", 8, "small bush 2", 5, "large bush 1", 2, "animal burrow", 3, "hillock", 6);
341            case 20: //SeasonalForest
342            case 21:
343                return makeShuffledRepeats(rng, "deciduous tree 1", 15, "deciduous tree 2", 13, "deciduous tree 3", 12, "small bush 1", 3, "large bush 1", 5, "large bush 2", 4, "animal burrow", 3);
344            case 27: //TemperateRainforest
345            case 33:
346                return makeShuffledRepeats(rng, "tropical tree 1", 6, "tropical tree 2", 5, "deciduous tree 1", 13, "deciduous tree 2", 12, "small bush 1", 8, "large bush 1", 7, "large bush 2", 7, "large bush 3", 3, "animal burrow", 3);
347            case 28: //TropicalRainforest
348            case 34:
349            case 35:
350                return makeShuffledRepeats(rng, "tropical tree 1", 12, "tropical tree 2", 11, "tropical tree 3", 10, "tropical tree 4", 9, "small bush 1", 6, "small bush 2", 5, "large bush 1", 6, "large bush 2", 5, "large bush 3", 3, "animal burrow", 9, "boulder", 1);
351            case 36: // Rocky
352            case 37:
353                return makeShuffledRepeats(rng, "seashell 1", 3, "seashell 2", 2, "seashell 3", 2, "driftwood", 6, "boulder", 9);
354            default:
355                return new ArrayList<>(0);
356        }
357    }
358//                //COLDEST //COLDER        //COLD            //HOT                  //HOTTER              //HOTTEST
359//                "Ice",    "Ice",          "Grassland",      "Desert",              "Desert",             "Desert",             //DRYEST
360//                "Ice",    "Tundra",       "Grassland",      "Grassland",           "Desert",             "Desert",             //DRYER
361//                "Ice",    "Tundra",       "Woodland",       "Woodland",            "Savanna",            "Desert",             //DRY
362//                "Ice",    "Tundra",       "SeasonalForest", "SeasonalForest",      "Savanna",            "Savanna",            //WET
363//                "Ice",    "Tundra",       "BorealForest",   "TemperateRainforest", "TropicalRainforest", "Savanna",            //WETTER
364//                "Ice",    "BorealForest", "BorealForest",   "TemperateRainforest", "TropicalRainforest", "TropicalRainforest", //WETTEST
365//                "Rocky",  "Rocky",        "Beach",          "Beach",               "Beach",              "Beach",              //COASTS
366//                "Ice",    "River",        "River",          "River",               "River",              "River",              //RIVERS
367//                "Ice",    "River",        "River",          "River",               "River",              "River",              //LAKES
368//                "Ocean",  "Ocean",        "Ocean",          "Ocean",               "Ocean",              "Ocean",              //OCEAN
369//                "Empty",                                                                                                       //SPACE
370    
371    public WildMap()
372    {
373        this(128, 128, 21);
374    }
375    public WildMap(int width, int height, int biome)
376    {
377        this(width, height, biome, new SilkRNG());
378    }
379    public WildMap(int width, int height, int biome, int seedA, int seedB)
380    {
381        this(width, height, biome, new SilkRNG(seedA, seedB));
382    }
383    public WildMap(int width, int height, int biome, IStatefulRNG rng)
384    {
385        this(width, height, biome, rng, floorsByBiome(biome, rng), contentByBiome(biome, rng));
386    }
387    public WildMap(int width, int height, int biome, IStatefulRNG rng, ArrayList<String> contentTypes)
388    {
389        this(width, height, biome, rng, floorsByBiome(biome, rng), contentTypes);
390    }
391    public WildMap(int width, int height, int biome, IStatefulRNG rng, ArrayList<String> floorTypes, ArrayList<String> contentTypes)
392    {
393        this.width = width;
394        this.height = height;
395        this.biome = biome;
396        this.rng = rng;
397        content = ArrayTools.fill(-1, width, height);
398        floors = new int[width][height];
399        this.floorTypes = floorTypes;
400        this.contentTypes = contentTypes;
401    }
402
403    /**
404     * Produces a map by filling the {@link #floors} 2D array with indices into {@link #floorTypes}, and similarly
405     * filling the {@link #content} 2D array with indices into {@link #contentTypes}. You only need to call this method
406     * when you first generate a map with the specific parameters you want, such as biome, and later if you want another
407     * map with the same parameters.
408     * <br>
409     * Virtually all of this method is a wrapper around functionality provided by {@link BlueNoise}, adjusted to fit
410     * wilderness maps slightly.
411     */
412    public void generate() {
413        ArrayTools.fill(content, -1);
414        final int seed = rng.nextInt();//, otherSeed = rng.nextInt(), choice = seed + otherSeed & 15;
415        final int limit = contentTypes.size(), floorLimit = floorTypes.size();
416        int b;
417        BlueNoise.blueSpill(floors, floorLimit, rng);
418        for (int x = 0; x < width; x++) {
419            for (int y = 0; y < height; y++) {
420                if((b = BlueNoise.getChosen(x, y, seed) + 128) < limit)
421                    content[x][y] = b;
422                //floors[x][y] = (int)((FastNoise.instance.layered2D(x,  y, otherSeed, 2, 0x1p-5f) * 0.4999f + 0.5f) * (floorLimit - 1) + 0.25f + rng.nextFloat(0.5f));
423            }
424        }
425    }
426
427    /**
428     * A subclass of {@link WildMap} that serves as a ragged edge between 2, 3, or 4 WildMaps in a square intersection.
429     * You almost always supply 4 WildMaps to this (typically not other MixedWildMaps), one for each corner of the map,
430     * and this generates an uneven border between them. Make sure to look up the indices in the {@link #content} and
431     * {@link #floors} using this MixedWildMap's {@link #contentTypes} and {@link #floorTypes}, not the ones in the
432     * inner WildMaps, because the indices in the MixedWildMap are different.
433     */
434    public static class MixedWildMap extends WildMap implements Serializable
435    {
436        private static final long serialVersionUID = 1L;
437        public final int[][] pieceMap;
438        public final WildMap[] pieces;
439        protected final int[] minFloors, maxFloors, minContents, maxContents;
440        
441        public MixedWildMap()
442        {
443            this(new WildMap(), new WildMap(), new WildMap(), new WildMap(), new SilkRNG());
444        }
445        
446        public MixedWildMap(WildMap northeast, WildMap southeast, WildMap southwest, WildMap northwest, IStatefulRNG rng)
447        {
448            super(northeast.width, northeast.height, northeast.biome, rng, new ArrayList<>(northeast.floorTypes), new ArrayList<>(northeast.contentTypes));
449            minFloors = new int[4];
450            maxFloors = new int[4];
451            minContents = new int[4];
452            maxContents = new int[4];
453            floorTypes.addAll(southeast.floorTypes);
454            floorTypes.addAll(southwest.floorTypes);
455            floorTypes.addAll(northwest.floorTypes);
456            contentTypes.addAll(southeast.contentTypes);
457            contentTypes.addAll(southwest.contentTypes);
458            contentTypes.addAll(northwest.contentTypes);
459            minFloors[1]   = maxFloors[0]   = northeast.floorTypes.size();
460            minContents[1] = maxContents[0] = northeast.contentTypes.size();
461            minFloors[2]   = maxFloors[1]   = maxFloors[0]    + southeast.floorTypes.size();
462            minContents[2] = maxContents[1] = maxContents[0]  + southeast.contentTypes.size();
463            minFloors[3]   = maxFloors[2]   = maxFloors[1]    + southwest.floorTypes.size();
464            minContents[3] = maxContents[2] = maxContents[1]  + southwest.contentTypes.size();
465            maxFloors[3]   = maxFloors[2]    + northwest.floorTypes.size();
466            maxContents[3] = maxContents[2]  + northwest.contentTypes.size();
467            pieces = new WildMap[]{northeast, southeast, southwest, northwest};
468            pieceMap = new int[width][height];
469        }
470        
471        protected void preparePieceMap()
472        {
473            ArrayTools.fill(pieceMap, 255);
474            pieceMap[width - 1][0] = 0; // northeast
475            pieceMap[width - 1][height - 1] = 1; // southeast
476            pieceMap[0][height - 1] = 2; // southwest
477            pieceMap[0][0] = 3; //northwest
478            final int spillerLimit = 4;
479            final Direction[] dirs = Direction.CARDINALS;
480            Direction d;
481            int t, rx, ry, ctr;
482            int[] ox = new int[width], oy = new int[height];
483            boolean anySuccesses = false;
484            do {
485                ctr = 0;
486                rng.randomOrdering(width, ox);
487                rng.randomOrdering(height, oy);
488                for (int x = 0; x < width; x++) {
489                    rx = ox[x];
490                    for (int y = 0; y < height; y++) {
491                        ry = oy[y];
492                        if ((t = pieceMap[rx][ry]) < spillerLimit) {
493                            d = dirs[rng.next(2)];
494                            if (rx + d.deltaX >= 0 && rx + d.deltaX < width && ry + d.deltaY >= 0 && ry + d.deltaY < height &&
495                                    pieceMap[rx + d.deltaX][ry + d.deltaY] >= spillerLimit) {
496                                pieceMap[rx + d.deltaX][ry + d.deltaY] = t;
497                                ctr++;
498                            }
499                            d = dirs[rng.next(2)];
500                            if (rx + d.deltaX >= 0 && rx + d.deltaX < width && ry + d.deltaY >= 0 && ry + d.deltaY < height &&
501                                    pieceMap[rx + d.deltaX][ry + d.deltaY] >= spillerLimit) {
502                                pieceMap[rx + d.deltaX][ry + d.deltaY] = t;
503                                ctr++;
504                            }
505                        }
506
507                    }
508                }
509                if(!anySuccesses && ctr == 0)
510                {
511                    ArrayTools.fill(pieceMap, 0);
512                    return;
513                }
514                else
515                    anySuccesses = true;
516            } while (ctr > 0);
517            do {
518                ctr = 0;
519                rng.randomOrdering(width, ox);
520                rng.randomOrdering(height, oy);
521                for (int x = 0; x < width; x++) {
522                    rx = ox[x];
523                    for (int y = 0; y < height; y++) {
524                        ry = oy[y];
525                        if ((t = pieceMap[rx][ry]) < spillerLimit) {
526                            for (int i = 0; i < 4; i++) {
527                                d = dirs[i];
528                                if (rx + d.deltaX >= 0 && rx + d.deltaX < width && ry + d.deltaY >= 0 && ry + d.deltaY < height &&
529                                        pieceMap[rx + d.deltaX][ry + d.deltaY] >= spillerLimit) {
530                                    pieceMap[rx + d.deltaX][ry + d.deltaY] = t;
531                                    ctr++;
532                                }
533                            }
534                        }
535
536                    }
537                }
538            } while (ctr > 0);
539
540        }
541
542        @Override
543        public void generate() {
544            ArrayTools.fill(content, -1);
545            for (int i = 0; i < pieces.length; i++) {
546                pieces[i].generate();
547            }
548            preparePieceMap();
549            int p, c;
550            WildMap piece;
551            for (int x = 0; x < width; x++) {
552                for (int y = 0; y < height; y++) {
553                    p = pieceMap[x][y];
554                    piece = pieces[p];
555                    floors[x][y] = piece.floors[x][y] + minFloors[p];
556                    if((c = piece.content[x][y]) >= 0)
557                    {
558                        content[x][y] = c + minContents[p];
559                    }
560                }
561            }
562        }
563    }
564}