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}