001package squidpony.squidgrid.mapping;
002
003import squidpony.*;
004import squidpony.squidgrid.Direction;
005import squidpony.squidmath.*;
006
007import java.io.Serializable;
008import java.util.ArrayList;
009import java.util.Collection;
010import java.util.Collections;
011import java.util.HashSet;
012
013/**
014 * When you have a world map as produced by {@link WorldMapGenerator}, you may want to fill it with claims by various
015 * factions, where each faction may be hand-made and may consist of humans or some fantasy species, such as goblins,
016 * elves, or demons. This can assign contiguous areas of land to various factions, while acknowledging any preferences
017 * some species may have for specific types of land (elves may strongly prefer forest terrain, or flying demons may be
018 * the ideal residents for difficult mountainous terrain). This needs both a {@link WorldMapGenerator} and a
019 * {@link squidpony.squidgrid.mapping.WorldMapGenerator.BiomeMapper} to allocate biomes and height/moisture info.
020 * The WorldMapGenerator is commonly a {@link squidpony.squidgrid.mapping.WorldMapGenerator.SphereMap} or a
021 * {@link squidpony.squidgrid.mapping.WorldMapGenerator.EllipticalMap} for a world map because they are fairly familiar
022 * map projections ({@link squidpony.squidgrid.mapping.WorldMapGenerator.HyperellipticalMap} can look better if
023 * important areas are in the corners of the rectangular area, but it's less familiar). If you're making a political map
024 * for an island or isolated area, then you may want {@link squidpony.squidgrid.mapping.WorldMapGenerator.LocalMap} or
025 * {@link squidpony.squidgrid.mapping.WorldMapGenerator.LocalMimicMap} instead, which don't have world-scale features
026 * like polar ice caps or a warm equator.
027 */
028public class FantasyPoliticalMapper implements Serializable {
029    private static final long serialVersionUID = 0L;
030
031    /**
032     * Represents a group that claims territory on a world-map, such as a nation. Each Faction has a name, a short name
033     * that may be the same as the regular name, a FakeLanguageGen that would be used to generate place names in that
034     * Faction's territory, and a number of (possibly null) arrays and Sets that represent what terrains this Faction
035     * wants to claim and which it will avoid (e.g. elves may prefer claiming forests, while frost giants won't claim
036     * anywhere that's even remotely warm).
037     */
038    public static class Faction implements Serializable
039    {
040        private static final long serialVersionUID = 0L;
041
042        public String name, shortName;
043        public FakeLanguageGen language;
044        /**
045         * A HashSet of String keys, where each key is the name of a biome this Faction wants to occupy.
046         * May be null if no biomes are specifically preferred.
047         */
048        public HashSet<String> preferredBiomes;
049        /**
050         * A HashSet of String keys, where each key is the name of a biome this Faction will never occupy.
051         * May be null if all biomes are available, but unless this is specified in a constructor, the default will be
052         * to consider "Ocean" blocked.
053         */
054        public HashSet<String> blockedBiomes;
055
056        /**
057         * An int array of height codes that this Faction prefers; 0, 1, 2, and 3 are all oceans, while 4 is shoreline
058         * or low-lying land and higher numbers (up to 8, inclusive) are used for increasing elevations.
059         */
060        public int[] preferredHeight;
061        /**
062         * An int array of heat codes that this Faction prefers; typically a 6-code scale is used where 0, 1, and 2 are
063         * cold and getting progressively warmer, while 3, 4, and 5 are warm to warmest.
064         */
065        public int[] preferredHeat;
066        /**
067         * An int array of moisture codes that this Faction prefers; typically a 6-code scale is used where 0, 1, and 2
068         * are dry and getting progressively more precipitation, while 3, 4, and 5 are wet to wettest.
069         */
070        public int[] preferredMoisture;
071
072        /**
073         * Zero-arg constructor that sets the language to a random FakeLanguageGen (using
074         * {@link FakeLanguageGen#randomLanguage(long)}), then generates a name/shortName with that FakeLanguageGen, and
075         * makes the only blocked biome "Ocean".
076         */
077        public Faction()
078        {
079            language = FakeLanguageGen.randomLanguage(
080                    (long) ((Math.random() - 0.5) * 0x10000000000000L)
081                            ^ (long) (((Math.random() - 0.5) * 2.0) * 0x8000000000000000L));
082            shortName = name = language.word(true);
083            this.blockedBiomes = new HashSet<>(1, 0.75f);
084            blockedBiomes.add("Ocean");
085        }
086
087        /**
088         * Constructor that sets the language to the specified FakeLanguageGen, then generates a name/shortName with
089         * that FakeLanguageGen, and makes the only blocked biome "Ocean".
090         * @param language the FakeLanguageGen to use for generating the name of the Faction and potentially place names
091         */
092        public Faction(FakeLanguageGen language)
093        {
094            this.language = language;
095            shortName = name = language.word(true);
096            this.blockedBiomes = new HashSet<>(1, 0.75f);
097            blockedBiomes.add("Ocean");
098        }
099        /**
100         * Constructor that sets the language to the specified FakeLanguageGen, sets the name and shortName to the
101         * specified name, and makes the only blocked biome "Ocean".
102         * @param language the FakeLanguageGen to use for potentially generating place names
103         * @param name the name of the Faction, such as "The United States of America"; will also be the shortName
104         */
105        public Faction(FakeLanguageGen language, String name)
106        {
107            this.language = language;
108            shortName = this.name = name;
109            this.blockedBiomes = new HashSet<>(1, 0.75f);
110            blockedBiomes.add("Ocean");
111        }
112        /**
113         * Constructor that sets the language to the specified FakeLanguageGen, sets the name to the specified name and
114         * the shortName to the specified shortName, and makes the only blocked biome "Ocean".
115         * @param language the FakeLanguageGen to use for potentially generating place names
116         * @param name the name of the Faction, such as "The United States of America"
117         * @param shortName the short name of the Faction, such as "America"
118         */
119        public Faction(FakeLanguageGen language, String name, String shortName)
120        {
121            this.language = language;
122            this.name = name;
123            this.shortName = shortName;
124            this.blockedBiomes = new HashSet<>(1, 0.75f);
125            blockedBiomes.add("Ocean");
126        }
127        /**
128         * Constructor that sets the language to the specified FakeLanguageGen, sets the name to the specified name and
129         * the shortName to the specified shortName, sets the preferredBiomes to be a Set containing the given Strings
130         * in preferredBiomes, and makes the only blocked biome "Ocean". The exact String names that are viable for
131         * biomes can be obtained from a BiomeMapper with {@link WorldMapGenerator.BiomeMapper#getBiomeNameTable()}.
132         * @param language the FakeLanguageGen to use for potentially generating place names
133         * @param name the name of the Faction, such as "The United States of America"
134         * @param shortName the short name of the Faction, such as "America"
135         * @param preferredBiomes a String array of biome names that this Faction prefers, typically taken from a BiomeMapper's {@link WorldMapGenerator.BiomeMapper#getBiomeNameTable()} value
136         * 
137         */
138        public Faction(FakeLanguageGen language, String name, String shortName, String[] preferredBiomes)
139        {
140            this.language = language;
141            this.name = name;
142            this.shortName = shortName;
143            this.preferredBiomes = new HashSet<>(preferredBiomes.length, 0.75f);
144            Collections.addAll(this.preferredBiomes, preferredBiomes);
145            this.blockedBiomes = new HashSet<>(1, 0.75f);
146            blockedBiomes.add("Ocean");
147        }
148        /**
149         * Constructor that sets the language to the specified FakeLanguageGen, sets the name to the specified name and
150         * the shortName to the specified shortName, sets the preferredBiomes to be a Set containing the given Strings
151         * in preferredBiomes, and sets the blocked biomes to be a Set containing exactly the given Strings in
152         * blockedBiomes. The exact String names that are viable for biomes can be obtained from a BiomeMapper with
153         * {@link WorldMapGenerator.BiomeMapper#getBiomeNameTable()}.
154         * @param language the FakeLanguageGen to use for potentially generating place names
155         * @param name the name of the Faction, such as "The United States of America"
156         * @param shortName the short name of the Faction, such as "America"
157         * @param preferredBiomes a String array of biome names that this Faction prefers, typically taken from a BiomeMapper's {@link WorldMapGenerator.BiomeMapper#getBiomeNameTable()} value
158         * @param blockedBiomes a String array of biome names that this Faction will never claim; if empty, this Faction may claim oceans
159         */
160        public Faction(FakeLanguageGen language, String name, String shortName, String[] preferredBiomes, String[] blockedBiomes)
161        {
162            this.language = language;
163            this.name = name;
164            this.shortName = shortName;
165            this.preferredBiomes = new HashSet<>(preferredBiomes.length, 0.75f);
166            Collections.addAll(this.preferredBiomes, preferredBiomes);
167            this.blockedBiomes = new HashSet<>(blockedBiomes.length, 0.75f);
168            Collections.addAll(this.blockedBiomes, blockedBiomes);
169        }
170        /**
171         * Constructor that sets the language to the specified FakeLanguageGen, sets the name to the specified name and
172         * the shortName to the specified shortName, sets the preferredBiomes to be a Set containing the given Strings
173         * in preferredBiomes, sets the blocked biomes to be a Set containing exactly the given Strings in
174         * blockedBiomes, and sets the preferred height codes to the ints in preferredHeight (with 4 being sea level and
175         * 8 being the highest peaks). The exact String names that are viable for biomes can be obtained from a
176         * BiomeMapper with {@link WorldMapGenerator.BiomeMapper#getBiomeNameTable()}.
177         * @param language the FakeLanguageGen to use for potentially generating place names
178         * @param name the name of the Faction, such as "The United States of America"
179         * @param shortName the short name of the Faction, such as "America"
180         * @param preferredBiomes a String array of biome names that this Faction prefers, typically taken from a BiomeMapper's {@link WorldMapGenerator.BiomeMapper#getBiomeNameTable()} value
181         * @param blockedBiomes a String array of biome names that this Faction will never claim; if empty, this Faction may claim oceans
182         * @param preferredHeight an int array of height codes this Faction prefers to claim; 4 is sea level and 8 is highest
183         */
184        public Faction(FakeLanguageGen language, String name, String shortName, String[] preferredBiomes, String[] blockedBiomes, int[] preferredHeight)
185        {
186            this.language = language;
187            this.name = name;
188            this.shortName = shortName;
189            this.preferredBiomes = new HashSet<>(preferredBiomes.length, 0.75f);
190            Collections.addAll(this.preferredBiomes, preferredBiomes);
191            this.blockedBiomes = new HashSet<>(blockedBiomes.length, 0.75f);
192            Collections.addAll(this.blockedBiomes, blockedBiomes);
193            this.preferredHeight = preferredHeight;
194        }
195
196        /**
197         * Constructor that sets the language to the specified FakeLanguageGen, sets the name to the specified name and
198         * the shortName to the specified shortName, sets the preferredBiomes to be a Set containing the given Strings
199         * in preferredBiomes, sets the blocked biomes to be a Set containing exactly the given Strings in
200         * blockedBiomes, sets the preferred height codes to the ints in preferredHeight (with 4 being sea level and 8
201         * being the highest peaks), and sets the preferred heat codes to the ints in preferredHeat (with the exact
202         * values depending on the BiomeMapper, but usually 0-5 range from coldest to hottest). The exact String names
203         * that are viable for biomes can be obtained from a BiomeMapper with
204         * {@link WorldMapGenerator.BiomeMapper#getBiomeNameTable()}.
205         * @param language the FakeLanguageGen to use for potentially generating place names
206         * @param name the name of the Faction, such as "The United States of America"
207         * @param shortName the short name of the Faction, such as "America"
208         * @param preferredBiomes a String array of biome names that this Faction prefers, typically taken from a BiomeMapper's {@link WorldMapGenerator.BiomeMapper#getBiomeNameTable()} value
209         * @param blockedBiomes a String array of biome names that this Faction will never claim; if empty, this Faction may claim oceans
210         * @param preferredHeight an int array of height codes this Faction prefers to claim; 4 is sea level and 8 is highest
211         * @param preferredHeat an int array of heat codes this Faction prefers to claim; typically 0 is coldest and 5 is hottest
212         */
213        public Faction(FakeLanguageGen language, String name, String shortName, String[] preferredBiomes, String[] blockedBiomes,
214                       int[] preferredHeight, int[] preferredHeat)
215        {
216            this.language = language;
217            this.name = name;
218            this.shortName = shortName;
219            this.preferredBiomes = new HashSet<>(preferredBiomes.length, 0.75f);
220            Collections.addAll(this.preferredBiomes, preferredBiomes);
221            this.blockedBiomes = new HashSet<>(blockedBiomes.length, 0.75f);
222            Collections.addAll(this.blockedBiomes, blockedBiomes);
223            this.preferredHeight = preferredHeight;
224            this.preferredHeat = preferredHeat;
225        }
226        /**
227         * Constructor that sets the language to the specified FakeLanguageGen, sets the name to the specified name and
228         * the shortName to the specified shortName, sets the preferredBiomes to be a Set containing the given Strings
229         * in preferredBiomes, sets the blocked biomes to be a Set containing exactly the given Strings in
230         * blockedBiomes, sets the preferred height codes to the ints in preferredHeight (with 4 being sea level and 8
231         * being the highest peaks), sets the preferred heat codes to the ints in preferredHeat (with the exact values
232         * depending on the BiomeMapper, but usually 0-5 range from coldest to hottest), and sets the preferred moisture
233         * codes to the ints in preferredMoisture (withe the exact values depending on the BiomeMapper, but usually 0-5
234         * range from driest to wettest). The exact String names that are viable for biomes can be obtained from a
235         * BiomeMapper with {@link WorldMapGenerator.BiomeMapper#getBiomeNameTable()}.
236         * @param language the FakeLanguageGen to use for potentially generating place names
237         * @param name the name of the Faction, such as "The United States of America"
238         * @param shortName the short name of the Faction, such as "America"
239         * @param preferredBiomes a String array of biome names that this Faction prefers, typically taken from a BiomeMapper's {@link WorldMapGenerator.BiomeMapper#getBiomeNameTable()} value
240         * @param blockedBiomes a String array of biome names that this Faction will never claim; if empty, this Faction may claim oceans
241         * @param preferredHeight an int array of height codes this Faction prefers to claim; 4 is sea level and 8 is highest
242         * @param preferredHeat an int array of heat codes this Faction prefers to claim; typically 0 is coldest and 5 is hottest
243         * @param preferredMoisture an int array of moisture codes this Faction prefers to claim; typically 0 is driest and 5 is wettest
244         */
245        public Faction(FakeLanguageGen language, String name, String shortName, String[] preferredBiomes, String[] blockedBiomes,
246                       int[] preferredHeight, int[] preferredHeat, int[] preferredMoisture)
247        {
248            this.language = language;
249            this.name = name;
250            this.shortName = shortName;
251            this.preferredBiomes = new HashSet<>(preferredBiomes.length, 0.75f);
252            Collections.addAll(this.preferredBiomes, preferredBiomes);
253            this.blockedBiomes = new HashSet<>(blockedBiomes.length, 0.75f);
254            Collections.addAll(this.blockedBiomes, blockedBiomes);
255            this.preferredHeight = preferredHeight;
256            this.preferredHeat = preferredHeat;
257            this.preferredMoisture = preferredMoisture;
258        }
259    }
260
261    public int width;
262    public int height;
263    public StatefulRNG rng;
264    public String name;
265    public char[][] politicalMap;
266    public char[][] zoomedMap;
267    public WorldMapGenerator wmg;
268    public WorldMapGenerator.BiomeMapper biomeMapper;
269    private static final ArrayList<Character> letters = Maker.makeList(
270            '~', '%', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y',
271            'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z',
272            'À', 'Á', 'Â', 'Ã', 'Ä', 'Å', 'Æ', 'Ç', 'È', 'É', 'Ê', 'Ë', 'Ì', 'Í', 'Î', 'Ï', 'Ð', 'Ñ', 'Ò', 'Ó', 'Ô', 'Õ', 'Ö', 'Ø', 'Ù', 'Ú', 'Û',
273            'Ü', 'Ý', 'Þ', 'ß', 'à', 'á', 'â', 'ã', 'ä', 'å', 'æ', 'ç', 'è', 'é', 'ê', 'ë', 'ì', 'í', 'î', 'ï', 'ð', 'ñ', 'ò', 'ó', 'ô', 'õ', 'ö',
274            'ø', 'ù', 'ú', 'û', 'ü', 'ý', 'þ', 'ÿ', 'Ā', 'ā', 'Ă', 'ă', 'Ą', 'ą', 'Ć', 'ć', 'Ĉ', 'ĉ', 'Ċ', 'ċ', 'Č', 'č', 'Ď', 'ď', 'Đ', 'đ', 'Ē',
275            'ē', 'Ĕ', 'ĕ', 'Ė', 'ė', 'Ę', 'ę', 'Ě', 'ě', 'Ĝ', 'ĝ', 'Ğ', 'ğ', 'Ġ', 'ġ', 'Ģ', 'ģ', 'Ĥ', 'ĥ', 'Ħ', 'ħ', 'Ĩ', 'ĩ', 'Ī', 'ī', 'Ĭ', 'ĭ',
276            'Į', 'į', 'İ', 'ı', 'Ĵ', 'ĵ', 'Ķ', 'ķ', 'ĸ', 'Ĺ', 'ĺ', 'Ļ', 'ļ', 'Ľ', 'ľ', 'Ŀ', 'ŀ', 'Ł', 'ł', 'Ń', 'ń', 'Ņ', 'ņ', 'Ň', 'ň', 'ʼn', 'Ō',
277            'ō', 'Ŏ', 'ŏ', 'Ő', 'ő', 'Œ', 'œ', 'Ŕ', 'ŕ', 'Ŗ', 'ŗ', 'Ř', 'ř', 'Ś', 'ś', 'Ŝ', 'ŝ', 'Ş', 'ş', 'Š', 'š', 'Ţ', 'ţ', 'Ť', 'ť', 'Ŧ', 'ŧ',
278            'Ũ', 'ũ', 'Ū', 'ū', 'Ŭ', 'ŭ', 'Ů', 'ů', 'Ű', 'ű', 'Ų', 'ų', 'Ŵ', 'ŵ', 'Ŷ', 'ŷ', 'Ÿ', 'Ź', 'ź', 'Ż', 'ż', 'Ž', 'ž', 'Ǿ', 'ǿ', 'Ș', 'ș',
279            'Ț', 'ț', 'Γ', 'Δ', 'Θ', 'Λ', 'Ξ', 'Π', 'Σ', 'Φ', 'Ψ', 'Ω', 'α');
280    /**
281     * Maps chars, as found in the returned array from generate(), to Strings that store the full name of nations.
282     */
283    public OrderedMap<Character, Faction> atlas;
284
285    /**
286     * Constructs a FantasyPoliticalMapper, but doesn't do anything with a map; you need to call
287     * {@link #generate(long, WorldMapGenerator, WorldMapGenerator.BiomeMapper, Collection, int, double)} for results.
288     */
289    public FantasyPoliticalMapper()
290    {
291        rng = new StatefulRNG();
292    }
293
294    /**
295     * For when you really don't care what arguments you give this, you can use this zero-parameter overload of
296     * generate() to produce a 128x128 {@link squidpony.squidgrid.mapping.WorldMapGenerator.TilingMap} world map with a
297     * {@link squidpony.squidgrid.mapping.WorldMapGenerator.SimpleBiomeMapper} biome mapper, filling it with 30 random
298     * Factions and trying to avoid unclaimed land. You may need to use {@link #atlas} to make sense of the randomly
299     * generated Factions. The seed will be random here.
300     * @return a 2D char array where each char can be used as a key into {@link #atlas} to find the Faction that claims it
301     */
302    public char[][] generate() {
303        wmg = new WorldMapGenerator.TilingMap(rng.nextLong(),128, 128);
304        wmg.generate();
305        biomeMapper = new WorldMapGenerator.SimpleBiomeMapper();
306        biomeMapper.makeBiomes(wmg);
307        return generate(rng.nextLong(), wmg, biomeMapper, null, 30, 1.0);
308    }
309
310    /**
311     * Generates a 2D char array that represents the claims to the land described by the WorldMapGenerator {@code wmg}
312     * and the BiomeMapper {@code biomeMapper} by various Factions, where {@link Faction} is an inner class.
313     * This starts with two default Factions for "Ocean" and "Wilderness" (unclaimed land) and adds randomly generated
314     * Factions to fill factionCount (the two default factions aren't counted against this limit). These Factions
315     * typically claim contiguous spans of land stretching out from a starting point that matches the Faction's
316     * preferences for biome, land height, heat, and moisture. If a Faction requires a biome (like "TropicalRainforest") 
317     * and the world has none of that type, then that Faction won't claim any land. If the WorldMapGenerator zooms in or
318     * out, you should call {@link #adjustZoom()} to get a different 2D char array that represents the zoomed-in area.
319     * This overload tries to claim all land that can be reached by an existing Faction, though islands will often be
320     * unclaimed.
321     * @param seed the seed that determines how Factions will randomly spread around the world
322     * @param wmg a WorldMapGenerator, which must have produced a map by calling its generate() method
323     * @param biomeMapper a WorldMapGenerator.BiomeMapper, which must have been initialized with wmg and refer to the same world
324     * @param factionCount the number of factions to have claiming land; cannot be negative or more than 253
325     * @return a 2D char array where each char can be used as a key into {@link #atlas} to find the Faction that claims it
326     */
327    public char[][] generate(long seed, WorldMapGenerator wmg, WorldMapGenerator.BiomeMapper biomeMapper,
328                             int factionCount) {
329        return generate(seed, wmg, biomeMapper, null, factionCount, 1.0);
330    }
331    /**
332     * Generates a 2D char array that represents the claims to the land described by the WorldMapGenerator {@code wmg}
333     * and the BiomeMapper {@code biomeMapper} by various Factions, where {@link Faction} is an inner class.
334     * This starts with two default Factions for "Ocean" and "Wilderness" (unclaimed land) and adds randomly generated
335     * Factions to fill factionCount (the two default factions aren't counted against this limit). These Factions
336     * typically claim contiguous spans of land stretching out from a starting point that matches the Faction's
337     * preferences for biome, land height, heat, and moisture. If a Faction requires a biome (like "TropicalRainforest") 
338     * and the world has none of that type, then that Faction won't claim any land. If the WorldMapGenerator zooms in or
339     * out, you should call {@link #adjustZoom()} to get a different 2D char array that represents the zoomed-in area.
340     * This overload tries to claim the given {@code controlledFraction} of land in total, though 1.0 can rarely be
341     * reached unless there are many factions and few islands.
342     * @param seed the seed that determines how Factions will randomly spread around the world
343     * @param wmg a WorldMapGenerator, which must have produced a map by calling its generate() method
344     * @param biomeMapper a WorldMapGenerator.BiomeMapper, which must have been initialized with wmg and refer to the same world
345     * @param factionCount the number of factions to have claiming land; cannot be negative or more than 253
346     * @param controlledFraction between 0.0 and 1.0 inclusive; higher means more land has a letter, lower has more '%'
347     * @return a 2D char array where each char can be used as a key into {@link #atlas} to find the Faction that claims it
348     */
349    public char[][] generate(long seed, WorldMapGenerator wmg, WorldMapGenerator.BiomeMapper biomeMapper,
350                             int factionCount, double controlledFraction) {
351        return generate(seed, wmg, biomeMapper, null, factionCount, controlledFraction);
352    }
353    /**
354     * Generates a 2D char array that represents the claims to the land described by the WorldMapGenerator {@code wmg}
355     * and the BiomeMapper {@code biomeMapper} by various Factions, where {@link Faction} is an inner class.
356     * This starts with two default Factions for "Ocean" and "Wilderness" (unclaimed land) and adds all of
357     * {@code factions} until {@code factionCount} is reached; if it isn't reached, random Factions will be generated to
358     * fill factionCount (the two default factions aren't counted against this limit). These Factions typically claim
359     * contiguous spans of land stretching out from a starting point that matches the Faction's preferences for biome,
360     * land height, heat, and moisture. If a Faction requires a biome (like "TropicalRainforest") and the world has none
361     * of that type, then that Faction won't claim any land. If the WorldMapGenerator zooms in or out, you should call
362     * {@link #adjustZoom()} to get a different 2D char array that represents the zoomed-in area. This overload tries to
363     * claim the given {@code controlledFraction} of land in total, though 1.0 can rarely be reached unless there are
364     * many factions and few islands.
365     *
366     * @param seed the seed that determines how Factions will randomly spread around the world
367     * @param wmg a WorldMapGenerator, which must have produced a map by calling its generate() method
368     * @param biomeMapper a WorldMapGenerator.BiomeMapper, which must have been initialized with wmg and refer to the same world
369     * @param factions a Collection of {@link Faction} that will be copied, shuffled and used before adding any random Factions
370     * @param factionCount the number of factions to have claiming land; cannot be negative or more than 253
371     * @param controlledFraction between 0.0 and 1.0 inclusive; higher means more land has a letter, lower has more '%'
372     * @return a 2D char array where each char can be used as a key into {@link #atlas} to find the Faction that claims it
373     */
374    public char[][] generate(long seed, WorldMapGenerator wmg, WorldMapGenerator.BiomeMapper biomeMapper,
375                                  Collection<Faction> factions, int factionCount, double controlledFraction) {
376        rng.setState(seed);
377        factionCount = Math.abs(factionCount % 254);
378        Thesaurus th = new Thesaurus(rng.nextLong());
379        ArrayList<Faction> fact = factions == null ? new ArrayList<Faction>() : rng.shuffle(factions);
380        for (int i = fact.size(); i < factionCount; i++) {
381            String name = th.makeNationName(), shortName = th.latestGenerated;
382            FakeLanguageGen lang;
383            if(th.randomLanguages == null || th.randomLanguages.isEmpty())
384                lang = FakeLanguageGen.randomLanguage(rng);
385            else
386                lang = th.randomLanguages.get(0);
387            fact.add(new Faction(lang, name, shortName));
388        }
389        if(factionCount > 0)
390            rng.shuffleInPlace(fact);
391        fact.add(0, new Faction(FakeLanguageGen.DEMONIC, "The Lost Wilderness", "Wilderness"));
392        fact.add(0, new Faction(FakeLanguageGen.ALIEN_O, "The Vast Domain of the Seafolk", "Seafolk", new String[]{"Ocean"}));
393        atlas = new OrderedMap<>(letters.subList(0, factionCount + 2), fact.subList(0, factionCount + 2), 0.5f);
394        this.wmg = wmg;
395        this.biomeMapper = biomeMapper;
396        width = wmg.width;
397        height = wmg.height;
398        GreasedRegion land = new GreasedRegion(wmg.heightCodeData, 4, 999);
399        politicalMap = land.toChars('%', '~');
400        int controlled = (int) (land.size() * Math.max(0.0, Math.min(1.0, controlledFraction)));
401
402        int[] centers = land.copy().randomScatter(rng, (int) (Math.sqrt(width * height) * 0.1 + 0.999), factionCount).asTightEncoded();
403        int cen, cx, cy, cx2, cy2, biome, high, hot, moist, count = centers.length, re;
404        String biomeName;
405        String[] biomeTable = biomeMapper.getBiomeNameTable();
406        int[] reorder = new int[count];
407        Faction current;
408        int[] factionIndices = new int[count];
409        for (int c = 0; c < count; c++) {
410            cen = centers[c];
411            cx = cen % width;
412            cy = cen / width;
413            biome = biomeMapper.getBiomeCode(cx, cy);
414            biomeName = biomeTable[biome];
415            high = wmg.heightCodeData[cx][cy];
416            hot = biomeMapper.getHeatCode(cx, cy);
417            moist = biomeMapper.getMoistureCode(cx, cy);
418            rng.randomOrdering(count, reorder);
419            PER_FACTION:
420            for (int i = 0; i < count; i++) {
421                current = fact.get(re = reorder[i]);
422                if(current.preferredBiomes == null || current.preferredBiomes.contains(biomeName))
423                {
424                    factionIndices[c] = re;
425                    break;
426                }
427                if(current.blockedBiomes != null && current.blockedBiomes.contains(biomeName))
428                    continue;
429                if(current.preferredHeight != null)
430                {
431                    for (int j = 0; j < current.preferredHeight.length; j++) {
432                        if(high == current.preferredHeight[j])
433                        {
434                            factionIndices[c] = re;
435                            break PER_FACTION;
436                        }
437                    }
438                }
439                if(current.preferredHeat != null)
440                {
441                    for (int j = 0; j < current.preferredHeat.length; j++) {
442                        if(hot == current.preferredHeat[j])
443                        {
444                            factionIndices[c] = re;
445                            break PER_FACTION;
446                        }
447                    }
448                }
449                if(current.preferredMoisture != null)
450                {
451                    for (int j = 0; j < current.preferredMoisture.length; j++) {
452                        if(moist == current.preferredMoisture[j])
453                        {
454                            factionIndices[c] = re;
455                            break PER_FACTION;
456                        }
457                    }
458                }
459            }
460        }
461        IntVLA[] fresh = new IntVLA[count];
462        int filled = 0;
463        boolean hasFresh = false;
464        int approximateArea = (controlled * 4) / (count * 3);
465        char[] keys = new char[count];
466        double[] biases = new double[count];
467        for (int i = 0; i < count; i++) {
468            fresh[i] = new IntVLA(approximateArea);
469            cen = centers[i];
470            fresh[i].add(cen);
471            cx = cen % width;
472            cy = cen / width;
473            politicalMap[cx][cy] = keys[i] = atlas.keyAt(factionIndices[i] + 2);
474            biases[i] = rng.nextDouble() * rng.nextDouble() + rng.nextDouble() + 0.03125;
475            hasFresh = true;
476        }
477        Direction[] dirs = Direction.CARDINALS;
478        IntVLA currentFresh;
479        GreasedRegion anySpillMap = new GreasedRegion(width, height),
480                anyFreshMap = new GreasedRegion(width, height);
481
482        while (hasFresh && filled < controlled) {
483            hasFresh = false;
484            for (int i = 0; i < count && filled < controlled; i++) {
485                currentFresh = fresh[i];
486                if (currentFresh.isEmpty())
487                    continue;
488                else
489                    hasFresh = true;
490                if (rng.nextDouble() < biases[i]) {
491                    int index = rng.nextIntHasty(currentFresh.size), cell = currentFresh.get(index);
492                    cx = cell % width;
493                    cy = cell / width;
494
495
496                    politicalMap[cx][cy] = keys[i];
497                    filled++;
498                    anySpillMap.insert(cx, cy);
499
500                    for (int d = 0; d < dirs.length; d++) {
501                        cx2 = wmg.wrapX(cx + dirs[d].deltaX, cy);
502                        cy2 = wmg.wrapY(cx, cy + dirs[d].deltaY);
503                        if (cx == cx2 && cy == cy2)
504                            continue;
505                        if (land.contains(cx2, cy2) && !anySpillMap.contains(cx2, cy2)) {
506                            if(!anyFreshMap.contains(cx2, cy2)) {
507                                currentFresh.add(cx2 + cy2 * width);
508                                anyFreshMap.insert(cx2, cy2);
509                            }
510                        }
511                    }
512                    currentFresh.removeIndex(index);
513                    anyFreshMap.remove(cx, cy);
514                }
515            }
516        }
517        zoomedMap = ArrayTools.copy(politicalMap);
518        return politicalMap;
519    }
520
521    /**
522     * If the WorldMapGenerator used by
523     * {@link #generate(long, WorldMapGenerator, WorldMapGenerator.BiomeMapper, Collection, int, double)} zooms in or
524     * out, you can call this method to make the {@link #zoomedMap} 2D char array match its zoom. The world-scale map,
525     * {@link #politicalMap}, will remain unchanged unless generate() is called again, but zoomedMap will change each
526     * time either generate() or adjustZoom() is called. This method isn't 100% precise on how it places borders; for
527     * aesthetic reasons, the borders are tattered with {@link GreasedRegion#fray(double)} so they don't look like a
528     * wall of angular bubbles. Using fray() at each level of zoom is quasi-random, so if you zoom in on the same
529     * sequence of points on two different occasions, the change from fray() will be the same, but it may be slightly
530     * different if any point of zoom is different.
531     * @return a direct reference to {@link #zoomedMap}, which will hold the correctly-zoomed version of {@link #politicalMap}
532     */
533    public char[][] adjustZoom() {
534        if(wmg.zoom <= 0)
535        {
536            return ArrayTools.insert(politicalMap, zoomedMap, 0, 0);
537        }
538        ArrayTools.fill(zoomedMap, ' ');
539        char c;
540        int stx = Math.min(Math.max((wmg.zoomStartX - (width  >> 1)) / ((2 << wmg.zoom) - 2), 0), width ),
541                sty = Math.min(Math.max((wmg.zoomStartY - (height >> 1)) / ((2 << wmg.zoom) - 2), 0), height);
542        GreasedRegion nation = new GreasedRegion(wmg.landData);
543        GreasedRegion fillable = new GreasedRegion(politicalMap, '~').not();
544        for (int i = 0; i < wmg.zoom; i++) {
545            fillable.zoom(stx, sty);
546        }
547        fillable.flood(nation, width + height);
548        for (int i = 1; i < atlas.size(); i++) {
549            nation.refill(politicalMap, c = atlas.keyAt(i));
550            if(nation.isEmpty()) continue;
551            for (int z = 0; z < wmg.zoom; z++) {
552                nation.zoom(stx, sty).expand8way().expand().fray(0.5);
553            }
554            fillable.andNot(nation);
555            nation.intoChars(zoomedMap, c);
556        }
557        for (int i = 1; i < atlas.size(); i++) {
558            nation.refill(zoomedMap, c = atlas.keyAt(i));
559            if(nation.isEmpty()) continue;
560            nation.flood(fillable, 4 << wmg.zoom).intoChars(zoomedMap, c);
561        }
562        nation.refill(wmg.heightCodeData, 4, 999).and(new GreasedRegion(zoomedMap, ' ')).intoChars(zoomedMap, '%');
563        nation.refill(wmg.heightCodeData, -999, 4).intoChars(zoomedMap, '~');
564        return zoomedMap;
565    }
566
567}