001package squidpony.squidgrid.mapping; 002 003import squidpony.ArrayTools; 004import squidpony.FakeLanguageGen; 005import squidpony.Maker; 006import squidpony.Thesaurus; 007import squidpony.squidgrid.Measurement; 008import squidpony.squidgrid.MultiSpill; 009import squidpony.squidmath.*; 010 011import java.util.ArrayList; 012import java.util.List; 013import java.util.Map; 014 015/** 016 * When you have a world map as produced by {@link WorldMapGenerator} or some other source, you may want to fill it with 017 * claims by various nations/factions, possibly procedural or possibly hand-made. This can assign contiguous areas of 018 * land to various factions, while ignoring some amount of "wild" land if desired, and keeping oceans unclaimed. 019 * The factions can be given procedural names in an atlas that is linked to the chars used by the world map. 020 * Uses {@link MultiSpill} internally to produce the semi-random nation shapes. Stores an {@link #atlas} that can be 021 * used to figure out what a char in a produced 2D char array means for its claiming nation, a {@link #briefAtlas} that 022 * will have short, identifiable parts of generated names corresponding to the same chars as in atlas, and an OrderedMap 023 * of {@link #spokenLanguages} that contains any randomly generated languages produced for nations. 024 * <a href="https://gist.github.com/tommyettinger/4a16a09bebed8e2fe8473c8ea444a2dd">Example output of a related class</a>. 025 * @see FantasyPoliticalMapper There's also an alternative for cases where non-human species compete for specific areas. 026 */ 027public class PoliticalMapper { 028 public int width; 029 public int height; 030 public StatefulRNG rng; 031 public String name; 032 public char[][] politicalMap; 033 public static final char[] letters = ArrayTools.letterSpan(256); 034 /** 035 * Maps chars, as found in the returned array from generate(), to Strings that store the full name of nations. 036 */ 037 public final OrderedMap<Character, String> atlas = new OrderedMap<>(32); 038 /** 039 * Maps chars, as found in the returned array from generate(), to Strings that store the short name of nations. 040 */ 041 public final OrderedMap<Character, String> briefAtlas = new OrderedMap<>(32); 042 /** 043 * Maps chars, as found in the returned array from generate(), to Strings that store the languages spoken in those 044 * nations, which could be user-assigned, unassigned, or randomly-generated. 045 */ 046 public final OrderedMap<Character, List<FakeLanguageGen>> spokenLanguages = new OrderedMap<>(32); 047 048 public PoliticalMapper() 049 { 050 name = "Permadeath Planet"; 051 rng = new StatefulRNG(CrossHash.hash64(name)); 052 } 053 /** 054 * Constructs a SpillWorldMap using the given world name, and uses the world name as the 055 * basis for all future random generation in this object. 056 * 057 * @param worldName a String name for the world that will be used as a seed for all random generation here 058 */ 059 public PoliticalMapper(String worldName) { 060 name = worldName; 061 rng = new StatefulRNG(CrossHash.hash64(name)); 062 } 063 /** 064 * Constructs a SpillWorldMap using the given world name, and uses the world name as the 065 * basis for all future random generation in this object. 066 * 067 * @param random an RNG to generate the name for the world in a random language, which will also serve as a seed 068 */ 069 public PoliticalMapper(IRNG random) { 070 this(FakeLanguageGen.SIMPLISH.word(random, true)); 071 } 072 /** 073 * Produces a political map for the land stored in the given WorldMapGenerator, with the given number 074 * of factions trying to take land in the world (essentially, nations). The output is a 2D char array where each 075 * letter char is tied to a different faction, while '~' is always water, and '%' is always wilderness or unclaimed 076 * land. The amount of unclaimed land is determined by the controlledFraction parameter, which will be clamped 077 * between 0.0 and 1.0, with higher numbers resulting in more land owned by factions and lower numbers meaning more 078 * wilderness. This version generates an atlas with the procedural names of all the factions and a 079 * mapping to the chars used in the output; the atlas will be in the {@link #atlas} member of this object. For every 080 * Character key in atlas, there will be a String value in atlas that is the name of the nation, and for the same 081 * key in {@link #spokenLanguages}, there will be a non-empty List of {@link FakeLanguageGen} languages (usually 082 * one, sometimes two) that should match any names generated for the nation. Ocean and Wilderness get the default 083 * FakeLanguageGen instances "ELF" and "DEMONIC", in case you need languages for those areas for some reason. 084 * @param wmg a WorldMapGenerator that has produced a map; this gets the land parts of the map to assign claims to, 085 * including rivers and lakes as part of nations but not oceans 086 * @param factionCount the number of factions to have claiming land, cannot be negative or more than 255 087 * @param controlledFraction between 0.0 and 1.0 inclusive; higher means more land has a letter, lower has more '%' 088 * @return a 2D char array where letters represent the claiming faction, '~' is water, and '%' is unclaimed 089 */ 090 public char[][] generate(WorldMapGenerator wmg, int factionCount, double controlledFraction) { 091 return generate(new GreasedRegion(wmg.heightCodeData, 4, 999), factionCount, controlledFraction); 092 } 093 /** 094 * Produces a political map for the land stored in the "on" cells of the given GreasedRegion, with the given number 095 * of factions trying to take land in the world (essentially, nations). The output is a 2D char array where each 096 * letter char is tied to a different faction, while '~' is always water, and '%' is always wilderness or unclaimed 097 * land. The amount of unclaimed land is determined by the controlledFraction parameter, which will be clamped 098 * between 0.0 and 1.0, with higher numbers resulting in more land owned by factions and lower numbers meaning more 099 * wilderness. This version generates an atlas with the procedural names of all the factions and a 100 * mapping to the chars used in the output; the atlas will be in the {@link #atlas} member of this object. For every 101 * Character key in atlas, there will be a String value in atlas that is the name of the nation, and for the same 102 * key in {@link #spokenLanguages}, there will be a non-empty List of {@link FakeLanguageGen} languages (usually 103 * one, sometimes two) that should match any names generated for the nation. Ocean and Wilderness get the default 104 * FakeLanguageGen instances "ELF" and "DEMONIC", in case you need languages for those areas for some reason. 105 * @param land a GreasedRegion that stores "on" cells for land and "off" cells for anything un-claimable, like ocean 106 * @param factionCount the number of factions to have claiming land, cannot be negative or more than 255 107 * @param controlledFraction between 0.0 and 1.0 inclusive; higher means more land has a letter, lower has more '%' 108 * @return a 2D char array where letters represent the claiming faction, '~' is water, and '%' is unclaimed 109 */ 110 public char[][] generate(GreasedRegion land, int factionCount, double controlledFraction) { 111 factionCount &= 255; 112 width = land.width; 113 height = land.height; 114 MultiSpill spreader = new MultiSpill(new short[width][height], Measurement.MANHATTAN, rng); 115 Coord.expandPoolTo(width, height); 116 GreasedRegion map = land.copy(); 117 //Coord[] centers = map.randomSeparated(0.1, rng, factionCount); 118 int controlled = (int) (map.size() * Math.max(0.0, Math.min(1.0, controlledFraction))); 119 map.randomScatter(rng, (width + height) / 25, factionCount); 120 121 spreader.initialize(land.toChars()); 122 OrderedMap<Coord, Double> entries = new OrderedMap<>(); 123 entries.put(Coord.get(-1, -1), 0.0); 124 for (int i = 0; i < factionCount; i++) { 125 entries.put(map.nth(i), rng.between(0.5, 1.0)); 126 } 127 spreader.start(entries, controlled, null); 128 short[][] sm = spreader.spillMap; 129 politicalMap = new char[width][height]; 130 for (int x = 0; x < width; x++) { 131 for (int y = 0; y < height; y++) { 132 politicalMap[x][y] = (sm[x][y] == -1) ? '~' : (sm[x][y] == 0) ? '%' : letters[(sm[x][y] - 1) & 255]; 133 } 134 } 135 136 atlas.clear(); 137 briefAtlas.clear(); 138 spokenLanguages.clear(); 139 atlas.put('~', "Ocean"); 140 briefAtlas.put('~', "Ocean"); 141 spokenLanguages.put('~', Maker.makeList(FakeLanguageGen.ELF)); 142 atlas.put('%', "Wilderness"); 143 briefAtlas.put('%', "Wilderness"); 144 spokenLanguages.put('%', Maker.makeList(FakeLanguageGen.DEMONIC)); 145 146 if (factionCount > 0) { 147 Thesaurus th = new Thesaurus(rng.nextLong()); 148 for (int i = 0; i < factionCount; i++) { 149 atlas.put(letters[i], th.makeNationName()); 150 briefAtlas.put(letters[i], th.latestGenerated); 151 if(th.randomLanguages == null || th.randomLanguages.isEmpty()) 152 spokenLanguages.put(letters[i], Maker.makeList(FakeLanguageGen.randomLanguage(rng))); 153 else 154 spokenLanguages.put(letters[i], new ArrayList<>(th.randomLanguages)); 155 } 156 } 157 return politicalMap; 158 } 159 /** 160 * Produces a political map for the land stored in the given WorldMapGenerator, with the given number 161 * of factions trying to take land in the world (essentially, nations). The output is a 2D char array where each 162 * letter char is tied to a different faction, while '~' is always water, and '%' is always wilderness or unclaimed 163 * land. The amount of unclaimed land is determined by the controlledFraction parameter, which will be clamped 164 * between 0.0 and 1.0, with higher numbers resulting in more land owned by factions and lower numbers meaning more 165 * wilderness. This version uses an existing atlas and does not assign to {@link #spokenLanguages}; it does not 166 * alter the existingAtlas parameter but uses it to determine what should be in this class' {@link #atlas} field. 167 * The atlas field will always contain '~' as the first key in its ordering (with value "Ocean" if no value was 168 * already assigned in existingAtlas to that key), and '%' as the second key (with value "Wilderness" if not already 169 * assigned); later entries will be taken from existingAtlas (not duplicating '~' or '%', but using the rest). 170 * @param wmg a WorldMapGenerator that has produced a map; this gets the land parts of the map to assign claims to, 171 * including rivers and lakes as part of nations but not oceans 172 * @param existingAtlas a Map (ideally an OrderedMap) of Character keys to be used in the 2D array, to String values 173 * that are the names of nations; should not have size greater than 255 174 * @param controlledFraction between 0.0 and 1.0 inclusive; higher means more land has a letter, lower has more '%' 175 * @return a 2D char array where letters represent the claiming faction, '~' is water, and '%' is unclaimed 176 */ 177 public char[][] generate(WorldMapGenerator wmg, Map<Character, String> existingAtlas, double controlledFraction) { 178 return generate(new GreasedRegion(wmg.heightCodeData, 4, 999), existingAtlas, controlledFraction); 179 } 180 /** 181 * Produces a political map for the land stored in the "on" cells of the given GreasedRegion, with the given number 182 * of factions trying to take land in the world (essentially, nations). The output is a 2D char array where each 183 * letter char is tied to a different faction, while '~' is always water, and '%' is always wilderness or unclaimed 184 * land. The amount of unclaimed land is determined by the controlledFraction parameter, which will be clamped 185 * between 0.0 and 1.0, with higher numbers resulting in more land owned by factions and lower numbers meaning more 186 * wilderness. This version uses an existing atlas and does not assign to {@link #spokenLanguages}; it does not 187 * alter the existingAtlas parameter but uses it to determine what should be in this class' {@link #atlas} field. 188 * The atlas field will always contain '~' as the first key in its ordering (with value "Ocean" if no value was 189 * already assigned in existingAtlas to that key), and '%' as the second key (with value "Wilderness" if not already 190 * assigned); later entries will be taken from existingAtlas (not duplicating '~' or '%', but using the rest). 191 * @param land a GreasedRegion that stores "on" cells for land and "off" cells for anything un-claimable, like ocean 192 * @param existingAtlas a Map (ideally an OrderedMap) of Character keys to be used in the 2D array, to String values 193 * that are the names of nations; should not have size greater than 255 194 * @param controlledFraction between 0.0 and 1.0 inclusive; higher means more land has a letter, lower has more '%' 195 * @return a 2D char array where letters represent the claiming faction, '~' is water, and '%' is unclaimed 196 */ 197 public char[][] generate(GreasedRegion land, Map<Character, String> existingAtlas, double controlledFraction) { 198 atlas.clear(); 199 briefAtlas.clear(); 200 atlas.putAll(existingAtlas); 201 if(atlas.getAndMoveToFirst('%') == null) 202 atlas.putAndMoveToFirst('%', "Wilderness"); 203 if(atlas.getAndMoveToFirst('~') == null) 204 atlas.putAndMoveToFirst('~', "Ocean"); 205 int factionCount = atlas.size() - 2; 206 briefAtlas.putAll(atlas); 207 width = land.width; 208 height = land.height; 209 MultiSpill spreader = new MultiSpill(new short[width][height], Measurement.MANHATTAN, rng); 210 Coord.expandPoolTo(width, height); 211 GreasedRegion map = land.copy(); 212 //Coord[] centers = map.randomSeparated(0.1, rng, factionCount); 213 int controlled = (int) (map.size() * Math.max(0.0, Math.min(1.0, controlledFraction))); 214 map.randomScatter(rng, (width + height) / 25, factionCount); 215 216 spreader.initialize(land.toChars()); 217 OrderedMap<Coord, Double> entries = new OrderedMap<>(); 218 entries.put(Coord.get(-1, -1), 0.0); 219 for (int i = 0; i < factionCount; i++) { 220 entries.put(map.nth(i), rng.between(0.5, 1.0)); 221 } 222 spreader.start(entries, controlled, null); 223 short[][] sm = spreader.spillMap; 224 politicalMap = new char[width][height]; 225 for (int x = 0; x < width; x++) { 226 for (int y = 0; y < height; y++) { 227 politicalMap[x][y] = (sm[x][y] == -1) ? '~' : (sm[x][y] == 0) ? '%' : atlas.keyAt((sm[x][y] + 1)); 228 } 229 } 230 return politicalMap; 231 } 232 /** 233 * Produces a political map for the land stored in the given WorldMapGenerator, with the given number 234 * of factions trying to take land in the world (essentially, nations). The output is a 2D char array where each 235 * letter char is tied to a different faction, while '~' is always water, and '%' is always wilderness or unclaimed 236 * land. The amount of unclaimed land is determined by the controlledFraction parameter, which will be clamped 237 * between 0.0 and 1.0, with higher numbers resulting in more land owned by factions and lower numbers meaning more 238 * wilderness. This version uses a "recipe for an atlas" instead of a complete atlas; this is an OrderedMap of 239 * Character keys to FakeLanguageGen values, where each key represents a faction and each value is the language to 240 * use to generate names for that faction. This does assign to {@link #spokenLanguages}, but it doesn't change the 241 * actual FakeLanguageGen objects, since they are immutable. It may add some "factions" if not present to represent 242 * oceans and unclaimed land. The atlas field will always contain '~' as the first key in its ordering (with value 243 * "Ocean" if no value was already assigned in existingAtlas to that key, or a random nation name in the language 244 * that was mapped if there is one), and '%' as the second key (with value "Wilderness" if not already assigned, or 245 * a similar random nation name if there is one); later entries will be taken from existingAtlas (not duplicating 246 * '~' or '%', but using the rest). 247 * @param wmg a WorldMapGenerator that has produced a map; this gets the land parts of the map to assign claims to, 248 * including rivers and lakes as part of nations but not oceans 249 * @param atlasLanguages an OrderedMap of Character keys to be used in the 2D array, to FakeLanguageGen objects that 250 * will be used to generate names; should not have size greater than 255 251 * @param controlledFraction between 0.0 and 1.0 inclusive; higher means more land has a letter, lower has more '%' 252 * @return a 2D char array where letters represent the claiming faction, '~' is water, and '%' is unclaimed 253 */ 254 public char[][] generate(WorldMapGenerator wmg, OrderedMap<Character, FakeLanguageGen> atlasLanguages, double controlledFraction) { 255 return generate(new GreasedRegion(wmg.heightCodeData, 4, 999), atlasLanguages, controlledFraction); 256 } 257 /** 258 * Produces a political map for the land stored in the "on" cells of the given GreasedRegion, with the given number 259 * of factions trying to take land in the world (essentially, nations). The output is a 2D char array where each 260 * letter char is tied to a different faction, while '~' is always water, and '%' is always wilderness or unclaimed 261 * land. The amount of unclaimed land is determined by the controlledFraction parameter, which will be clamped 262 * between 0.0 and 1.0, with higher numbers resulting in more land owned by factions and lower numbers meaning more 263 * wilderness. This version uses a "recipe for an atlas" instead of a complete atlas; this is an OrderedMap of 264 * Character keys to FakeLanguageGen values, where each key represents a faction and each value is the language to 265 * use to generate names for that faction. This does assign to {@link #spokenLanguages}, but it doesn't change the 266 * actual FakeLanguageGen objects, since they are immutable. It may add some "factions" if not present to represent 267 * oceans and unclaimed land. The atlas field will always contain '~' as the first key in its ordering (with value 268 * "Ocean" if no value was already assigned in existingAtlas to that key, or a random nation name in the language 269 * that was mapped if there is one), and '%' as the second key (with value "Wilderness" if not already assigned, or 270 * a similar random nation name if there is one); later entries will be taken from existingAtlas (not duplicating 271 * '~' or '%', but using the rest). 272 * @param land a GreasedRegion that stores "on" cells for land and "off" cells for anything un-claimable, like ocean 273 * @param atlasLanguages an OrderedMap of Character keys to be used in the 2D array, to FakeLanguageGen objects that 274 * will be used to generate names; should not have size greater than 255 275 * @param controlledFraction between 0.0 and 1.0 inclusive; higher means more land has a letter, lower has more '%' 276 * @return a 2D char array where letters represent the claiming faction, '~' is water, and '%' is unclaimed 277 */ 278 public char[][] generate(GreasedRegion land, OrderedMap<Character, FakeLanguageGen> atlasLanguages, double controlledFraction) { 279 atlas.clear(); 280 briefAtlas.clear(); 281 spokenLanguages.clear(); 282 283 Thesaurus th = new Thesaurus(rng.nextLong()); 284 FakeLanguageGen flg; 285 if((flg = atlasLanguages.get('~')) == null) { 286 atlas.put('~', "Ocean"); 287 briefAtlas.put('~', "Ocean"); 288 spokenLanguages.put('~', Maker.makeList(FakeLanguageGen.ELF)); 289 } 290 else { 291 atlas.put('~', th.makeNationName(flg)); 292 briefAtlas.put('~', th.latestGenerated); 293 spokenLanguages.put('~', Maker.makeList(flg)); 294 } 295 if((flg = atlasLanguages.get('%')) == null) { 296 atlas.put('%', "Wilderness"); 297 briefAtlas.put('%', "Wilderness"); 298 spokenLanguages.put('%', Maker.makeList(FakeLanguageGen.DEMONIC)); 299 } 300 else { 301 atlas.put('%', th.makeNationName(flg)); 302 briefAtlas.put('%', th.latestGenerated); 303 spokenLanguages.put('%', Maker.makeList(flg)); 304 } 305 306 for (int i = 0; i < atlasLanguages.size() && i < 256; i++) { 307 Character c = atlasLanguages.keyAt(i); 308 flg = atlasLanguages.getAt(i); 309 atlas.put(c, th.makeNationName(flg)); 310 briefAtlas.put(c, th.latestGenerated); 311 spokenLanguages.put(c, Maker.makeList(flg)); 312 } 313 int factionCount = atlas.size() - 2; 314 width = land.width; 315 height = land.height; 316 MultiSpill spreader = new MultiSpill(new short[width][height], Measurement.MANHATTAN, rng); 317 Coord.expandPoolTo(width, height); 318 GreasedRegion map = land.copy(); 319 //Coord[] centers = map.randomSeparated(0.1, rng, factionCount); 320 int controlled = (int) (map.size() * Math.max(0.0, Math.min(1.0, controlledFraction))); 321 map.randomScatter(rng, (width + height) / 25, factionCount); 322 323 spreader.initialize(land.toChars()); 324 OrderedMap<Coord, Double> entries = new OrderedMap<>(); 325 entries.put(Coord.get(-1, -1), 0.0); 326 for (int i = 0; i < factionCount; i++) { 327 entries.put(map.nth(i), rng.between(0.5, 1.0)); 328 } 329 spreader.start(entries, controlled, null); 330 short[][] sm = spreader.spillMap; 331 politicalMap = new char[width][height]; 332 for (int x = 0; x < width; x++) { 333 for (int y = 0; y < height; y++) { 334 politicalMap[x][y] = atlas.keyAt(sm[x][y] + 1); 335 } 336 } 337 return politicalMap; 338 } 339}