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}