001package squidpony.squidgrid.mapping;
002
003import squidpony.ArrayTools;
004import squidpony.Thesaurus;
005import squidpony.squidgrid.Measurement;
006import squidpony.squidgrid.MultiSpill;
007import squidpony.squidmath.*;
008
009import java.util.ArrayList;
010import java.util.Arrays;
011import java.util.Collections;
012
013/**
014 * Generates a procedural world map and fills it with the requested number of factions, keeping some land unclaimed.
015 * The factions are given procedural names in an atlas that is linked to the chars used by the world map.
016 * Uses MultiSpill internally to produce the semi-random land and water shapes, hence the name.
017 * <br>
018 * This class is probably fine for a lot of roguelike usage, but if you want a more realistic map and still want to
019 * distribute factions across it, consider {@link WorldMapGenerator}'s inner classes and {@link PoliticalMapper} or
020 * {@link FantasyPoliticalMapper} to place claims by factions. Most of the WorldMapGenerator maps allow wrapping around
021 * one axis (or both for {@link squidpony.squidgrid.mapping.WorldMapGenerator.TilingMap}), and allow calculating data
022 * for the ecosystems on the world map with {@link squidpony.squidgrid.mapping.WorldMapGenerator.DetailedBiomeMapper}.
023 * This class doesn't compute the height or moisture levels that DetailedBiomeMapper needs to figure out ecosystems.
024 * <br>
025 * <a href="https://gist.github.com/tommyettinger/4a16a09bebed8e2fe8473c8ea444a2dd">Example output</a>.
026 * Created by Tommy Ettinger on 9/12/2016.
027 */
028public class SpillWorldMap {
029    public int width;
030    public int height;
031    public StatefulRNG rng;
032    public String name;
033    public char[][] politicalMap;
034    public int[][] heightMap;
035    public Coord[] mountains;
036    public static final char[] letters = ArrayTools.letterSpan(256);
037    public final OrderedMap<Character, String> atlas = new OrderedMap<>(16);
038
039    public SpillWorldMap()
040    {
041        width = 20;
042        height = 20;
043        name = "Permadeath Island";
044        rng = new StatefulRNG(CrossHash.hash64(name));
045    }
046    /**
047     * Constructs a SpillWorldMap using the given width, height, and world name, and uses the world name as the
048     * basis for all future random generation in this object.
049     *
050     * @param width     the width of the map in cells
051     * @param height    the height of the map in cells
052     * @param worldName a String name for the world that will be used as a seed for all random generation here
053     */
054    public SpillWorldMap(int width, int height, String worldName) {
055        this.width = Math.max(width, 20);
056        this.height = Math.max(height, 20);
057        name = worldName;
058        rng = new StatefulRNG(CrossHash.hash64(name));
059    }
060
061    /**
062     * Generates a basic physical map for this world, then overlays a more involved political map with the given number
063     * of factions trying to take land in the world (essentially, nations). The output is a 2D char array where each
064     * letter char is tied to a different faction, while '~' is always water, and '%' is always wilderness or unclaimed
065     * land. A random amount, up to 10% of all land, will be unclaimed wilderness with this method; for more precise
066     * control, use the overload that takes a controlledFraction parameter and give it a value between 0.0 and 1.0,
067     * inclusive. If makeAtlas is true, this also generates an atlas with the procedural names of all the factions and a
068     * mapping to the chars used in the output; the atlas will be in the {@link #atlas} member of this object but will
069     * be empty if makeAtlas has never been true in a call to this.
070     * <br>
071     * If width or height is larger than 256, consider enlarging the Coord pool before calling this with
072     * {@code Coord.expandPoolTo(width, height);}. This will have no effect if width and height are both less than or
073     * equal to 256, but if you expect to be using maps that are especially large (which makes sense for world maps),
074     * expanding the pool will use more memory initially and then (possibly) much less over time, easing pressure on
075     * the garbage collector as well, as re-allocations of large Coords that would otherwise be un-cached are avoided.
076     * @param factionCount the number of factions to have claiming land, cannot be negative or more than 255
077     * @param makeAtlas if true, this will assign random names to factions, accessible via {@link #atlas}
078     * @return a 2D char array where letters represent the claiming faction, '~' is water, and '%' is unclaimed
079     */
080    public char[][] generate(int factionCount, boolean makeAtlas) {
081        return generate(factionCount, makeAtlas, false, rng.between(0.9, 1.0), 1.0);
082    }
083    /**
084     * Generates a basic physical map for this world, then overlays a more involved political map with the given number
085     * of factions trying to take land in the world (essentially, nations). The output is a 2D char array where each
086     * letter char is tied to a different faction, while '~' is always water, and '%' is always wilderness or unclaimed
087     * land. The amount of unclaimed land is determined by the controlledFraction parameter, which will be clamped
088     * between 0.0 and 1.0, with higher numbers resulting in more land owned by factions and lower numbers meaning more
089     * wilderness. If makeAtlas is true, it also generates an atlas with the procedural names of all the factions and a
090     * mapping to the chars used in the output; the atlas will be in the {@link #atlas} member of this object but will
091     * be empty if makeAtlas has never been true in a call to this.
092     * <br>
093     * If width or height is larger than 256, consider enlarging the Coord pool before calling this with
094     * {@code Coord.expandPoolTo(width, height);}. This will have no effect if width and height are both less than or
095     * equal to 256, but if you expect to be using maps that are especially large (which makes sense for world maps),
096     * expanding the pool will use more memory initially and then (possibly) much less over time, easing pressure on
097     * the garbage collector as well, as re-allocations of large Coords that would otherwise be un-cached are avoided.
098     * @param factionCount the number of factions to have claiming land, cannot be negative or more than 255
099     * @param makeAtlas if true, this will assign random names to factions, accessible via {@link #atlas}
100     * @param controlledFraction between 0.0 and 1.0 inclusive; higher means more land has a letter, lower has more '%'
101     * @return a 2D char array where letters represent the claiming faction, '~' is water, and '%' is unclaimed
102     */
103    public char[][] generate(int factionCount, boolean makeAtlas, double controlledFraction) {
104        return generate(factionCount, makeAtlas, false, controlledFraction, 1.0);
105    }
106    /**
107     * Generates a basic physical map for this world, then overlays a more involved political map with the given number
108     * of factions trying to take land in the world (essentially, nations). The output is a 2D char array where each
109     * letter char is tied to a different faction, while '~' is always water, and '%' is always wilderness or unclaimed
110     * land. The amount of unclaimed land is determined by the controlledFraction parameter, which will be clamped
111     * between 0.0 and 1.0, with higher numbers resulting in more land owned by factions and lower numbers meaning more
112     * wilderness. If makeAtlas is true, it also generates an atlas with the procedural names of all the factions and a
113     * mapping to the chars used in the output; the atlas will be in the {@link #atlas} member of this object but will
114     * be empty if makeAtlas has never been true in a call to this.
115     * <br>
116     * If width or height is larger than 256, consider enlarging the Coord pool before calling this with
117     * {@code Coord.expandPoolTo(width, height);}. This will have no effect if width and height are both less than or
118     * equal to 256, but if you expect to be using maps that are especially large (which makes sense for world maps),
119     * expanding the pool will use more memory initially and then (possibly) much less over time, easing pressure on
120     * the garbage collector as well, as re-allocations of large Coords that would otherwise be un-cached are avoided.
121     * @param factionCount the number of factions to have claiming land, cannot be negative or more than 255
122     * @param makeAtlas if true, this will assign random names to factions, accessible via {@link #atlas}
123     * @param controlledFraction between 0.0 and 1.0 inclusive; higher means more land has a letter, lower has more '%'
124     * @param waterStrength a non-negative multiplier that affects ocean size; 1 is more land than water
125     * @return a 2D char array where letters represent the claiming faction, '~' is water, and '%' is unclaimed
126     */
127    public char[][] generate(int factionCount, boolean makeAtlas, double controlledFraction, double waterStrength) {
128        return generate(factionCount, makeAtlas, false, controlledFraction, waterStrength);
129    }
130    /**
131     * Generates a basic physical map for this world, then overlays a more involved political map with the given number
132     * of factions trying to take land in the world (essentially, nations). The output is a 2D char array where each
133     * letter char is tied to a different faction, while '~' is always water, and '%' is always wilderness or unclaimed
134     * land. The amount of unclaimed land is determined by the controlledFraction parameter, which will be clamped
135     * between 0.0 and 1.0, with higher numbers resulting in more land owned by factions and lower numbers meaning more
136     * wilderness. If makeAtlas is true, it also generates an atlas with the procedural names of all the factions and a
137     * mapping to the chars used in the output; the atlas will be in the {@link #atlas} member of this object but will
138     * be empty if makeAtlas has never been true in a call to this. If makeHeight is true, this will generate a height
139     * map in an organic way (though it isn't especially fast on very large maps), assigning the height map as an
140     * int[][] to {@link #heightMap} and the potential mountains, hills, or peaks to the Coord[] {@link #mountains}. The
141     * first Coord in mountains is usually the tallest point on the map, though two or more small peaks that are very
142     * close to one another might fuse into one larger mountain range, with higher int values than those for the first
143     * mountain on its own.
144     * <br>
145     * If width or height is larger than 256, consider enlarging the Coord pool before calling this with
146     * {@code Coord.expandPoolTo(width, height);}. This will have no effect if width and height are both less than or
147     * equal to 256, but if you expect to be using maps that are especially large (which makes sense for world maps),
148     * expanding the pool will use more memory initially and then (possibly) much less over time, easing pressure on
149     * the garbage collector as well, as re-allocations of large Coords that would otherwise be un-cached are avoided.
150     * @param factionCount the number of factions to have claiming land, cannot be negative or more than 255
151     * @param makeAtlas if true, this will assign random names to factions, accessible via {@link #atlas}
152     * @param makeHeight if true, this will generate a height map, accessible via {@link #heightMap}, with -1 for water
153     * @param controlledFraction between 0.0 and 1.0 inclusive; higher means more land has a letter, lower has more '%'
154     * @param waterStrength a non-negative multiplier that affects ocean size; 1 is more land than water
155     * @return a 2D char array where letters represent the claiming faction, '~' is water, and '%' is unclaimed
156     */
157    public char[][] generate(int factionCount, boolean makeAtlas, boolean makeHeight, double controlledFraction, double waterStrength) {
158        factionCount &= 255;
159        //, extra = 25 + (height * width >>> 4);
160        MultiSpill spreader = new MultiSpill(new short[width][height], Measurement.MANHATTAN, rng);
161        GreasedRegion bounds = new GreasedRegion(width, height).not().retract(5),
162                smallerBounds = bounds.copy().retract(5), area = new GreasedRegion(width, height),
163                tmpEdge = new GreasedRegion(width, height), tmpInner = new GreasedRegion(width, height),
164                tmpOOB = new GreasedRegion(width, height);
165        Coord[] pts = smallerBounds.randomPortion(rng, rng.between(24, 48));
166        waterStrength = Math.max(0.0, waterStrength);
167        int aLen = pts.length;
168        short[][] sm = new short[width][height], tmpSM;
169        Arrays.fill(sm[0], (short) -1);
170        for (int i = 1; i < width; i++) {
171            System.arraycopy(sm[0], 0, sm[i], 0, height);
172        }
173        OrderedMap<Coord, Double> entries = new OrderedMap<>();
174        for (int i = 0; i < aLen; i++) {
175            int volume = 10 + (height * width) / (aLen);
176            area.empty().insert(pts[i]).spill(bounds, volume, rng).expand(3);
177            tmpInner.remake(area).retract(4).expand8way().and(area);
178            tmpEdge.remake(area).surface();
179            Coord[] edges = tmpEdge.mixedRandomSeparated(0.35);
180            int eLen = edges.length;
181            Double[] powers = new Double[eLen];
182            Arrays.fill(powers, 0.1 * waterStrength);
183            entries = new OrderedMap<>(edges, powers);
184            eLen = entries.size();
185            for (int j = 0; j < 32; j++) {
186                entries.put(tmpInner.singleRandom(rng), (rng.nextDouble() + 1.5) * 0.4);
187            }
188            tmpOOB.remake(area).not();
189
190            spreader.start(entries, -1, tmpOOB);
191            tmpSM = spreader.spillMap;
192
193            for (int x = 0; x < width; x++) {
194                for (int y = 0; y < height; y++) {
195                    if (tmpSM[x][y] >= eLen)
196                        sm[x][y] = 0;
197                }
198            }
199
200        }
201        /*
202        // old version; produced a strange "swiss cheese" world map layout.
203        SobolQRNG sobol = new SobolQRNG(2);
204        sobol.skipTo(rng.between(1000, 6500));
205        //sobol.fillVector(filler);
206        //entries.put(Coord.get((int)(dim * filler[0]), (int)(dim * filler[1])), (filler[2] + 0.25) / 1.25);
207        for (int j = 0; j < 8; j++) {
208            entries.put(sobol.nextCoord(width - 16, height - 16).add(8), 0.85);
209        }
210        for (int j = 8; j < count; j++) {
211            entries.put(sobol.nextCoord(width - 16, height - 16).add(8), (rng.nextDouble() + 0.25) * 0.3);
212        }
213        count = entries.size();
214        double edgePower = 0.95;// count * 0.8 / extra;
215        for (int x = 1; x < width - 1; x += 4) {
216            entries.put(Coord.get(x, 0), edgePower);
217            entries.put(Coord.get(x, height - 1), edgePower);
218        }
219        for (int y = 1; y < height - 1; y += 4) {
220            entries.put(Coord.get(0, y), edgePower);
221            entries.put(Coord.get(width - 1, y), edgePower);
222        }
223        spreader.start(entries, -1, null);
224        short[][] sm = spreader.spillMap;
225        for (int x = 0; x < width; x++) {
226            for (int y = 0; y < height; y++) {
227                //blank[x][y] = (char) ('a' + Integer.bitCount(sm[x][y] + 7) % 26);
228                if (sm[x][y] >= count || (sm[x][y] & 7) == 0)
229                    sm[x][y] = -1;
230                else
231                    sm[x][y] = 0;
232            }
233        }
234        */
235        GreasedRegion map = new GreasedRegion(sm, 0, 0x7fff);
236        Coord[] centers = map.mixedRandomSeparated(0.1, factionCount, rng.nextLong());
237        int controlled = (int) (map.size() * Math.max(0.0, Math.min(1.0, controlledFraction)));
238
239        spreader.initialize(sm);
240        entries.clear();
241        entries.put(Coord.get(-1, -1), 0.0);
242        for (int i = 0; i < factionCount; i++) {
243            entries.put(centers[i], rng.between(0.5, 1.0));
244        }
245        spreader.start(entries, controlled, null);
246        sm = spreader.spillMap;
247        politicalMap = new char[width][height];
248        for (int x = 0; x < width; x++) {
249            for (int y = 0; y < height; y++) {
250                politicalMap[x][y] = (sm[x][y] == -1) ? '~' : (sm[x][y] == 0) ? '%' : letters[(sm[x][y] - 1) & 255];
251            }
252        }
253        if (makeAtlas) {
254            atlas.clear();
255            atlas.put('~', "Water");
256            atlas.put('%', "Wilderness");
257            if(factionCount > 0) {
258                Thesaurus th = new Thesaurus(rng.nextLong());
259                for (int i = 0; i < factionCount; i++) {
260                    atlas.put(letters[i], th.makeNationName());
261                }
262            }
263        }
264        if(makeHeight)
265        {
266            GreasedRegion m2 = new GreasedRegion(map), g2;
267            map.retract(1);
268            OrderedSet<Coord> peaks = new OrderedSet<>(mountains = map.quasiRandomSeparated(0.4, rng.between(100, 150)));
269            int peakCount = peaks.size();
270            ArrayList<GreasedRegion> groups = new ArrayList<>(peakCount * 3);
271            for (int i = 0; i < peakCount; i++) {
272                groups.add(new GreasedRegion(width, height).insertSeveral(peaks).spill(map, peaks.size() * 17, rng));
273                peaks.removeLast();
274            }
275            Collections.addAll(peaks, map.randomPortion(rng, peakCount));
276            for (int i = 0; i < peakCount; i++) {
277                g2 = new GreasedRegion(width, height).insertSeveral(peaks).expand(4).deteriorate(rng, 2).and(m2);
278                groups.add(g2);
279                if(rng.nextBoolean())
280                    groups.add(g2);
281                peaks.clear();
282                Collections.addAll(peaks, map.randomPortion(rng, peakCount));
283            }
284            heightMap = GreasedRegion.sum(groups);
285            m2.not().writeIntsInto(heightMap, -1);
286        }
287        else
288            heightMap = new int[width][height];
289        return politicalMap;
290    }
291    /**
292     * Generates a basic physical map for this world, then overlays a more involved political map with the given number
293     * of factions trying to take land in the world (essentially, nations). The output is a 2D char array where each
294     * letter char is tied to a different faction, while '~' is always water, and '%' is always wilderness or unclaimed
295     * land. Does not generate an atlas, so you should come up with meanings for the letters yourself.
296     *
297     * @param factionCount the number of factions to have claiming land
298     * @return a 2D char array where letters represent the claiming faction, '~' is water, and '%' is unclaimed
299     */
300    public char[][] generate(int factionCount) {
301        return generate(factionCount, false);
302    }
303}