001package squidpony.squidgrid.mapping; 002 003import squidpony.ArrayTools; 004import squidpony.FakeLanguageGen; 005import squidpony.Thesaurus; 006import squidpony.annotation.Beta; 007import squidpony.squidgrid.Direction; 008import squidpony.squidmath.BlueNoise; 009import squidpony.squidmath.IRNG; 010import squidpony.squidmath.IStatefulRNG; 011import squidpony.squidmath.SilkRNG; 012 013import java.io.Serializable; 014import java.util.ArrayList; 015 016import static squidpony.Maker.makeList; 017 018/** 019 * A finite 2D area map for some kind of wilderness, adapting to different ecosystems by changing its output. 020 * Regional maps for wilderness areas have very different requirements from mostly-artificial dungeons. This is intended 021 * to work alongside {@link WorldMapGenerator} and {@link WorldMapGenerator.DetailedBiomeMapper} to produce, for 022 * example, very sparse maps with an occasional cactus in a desert, or very dense maps with many trees and shrubs for a 023 * forest. 024 * <br> 025 * Using this code mostly involves constructing a WildMap with a width, height, biome, and optionally a random number 026 * generator, an ArrayList of floor types (as Strings) that can appear, and an ArrayList of terrain content that can 027 * appear (also as Strings). Then you can call {@link #generate()}, which assigns indices into {@link #content} and 028 * {@link #floors}, where an index can look up a value from {@link #contentTypes} or {@link #floorTypes}. The biome is 029 * usually an index into {@link squidpony.squidgrid.mapping.WorldMapGenerator.DetailedBiomeMapper#biomeTable}, but can 030 * be some other index if you don't use DetailedBiomeMapper (you would probably use a subclass then). The 031 * {@link #contentTypes} field is an ArrayList; you can have and are encouraged to have duplicates when an object should 032 * appear more often. An index of -1 in content indicates nothing of note is present there. There is also a String array 033 * of {@link #floorTypes} that is not typically user-set unless you subclass WildMap yourself; it is used to look up the 034 * indices in {@link #floors}. The floors are set to reasonable values for the particular biome, so a forest has "dirt" 035 * and "leaves" among others, while a desert might only have "sand". Again, only the indices matter, so you could change 036 * the values in {@link #floorTypes} to match names of textures in a graphical game and make lookup easier, or to a char 037 * followed by the name of a color (as in SColor in the display module) for a text-based game. 038 * <br> 039 * This is marked Beta because there's still some work to be done, and the actual output will change even if the API 040 * doesn't have any breaks. While the wilderness maps this produces are usable, they don't have paths or areas that a 041 * character would have to find a way around (like a cliff). This is meant to be added at some point, probably in 042 * conjunction with some system for connecting WildMaps. 043 * <br> 044 * Created by Tommy Ettinger on 10/16/2019. 045 */ 046@Beta 047public class WildMap implements Serializable { 048 private static final long serialVersionUID = 1L; 049 public final int width, height; 050 public int biome; 051 public IStatefulRNG rng; 052 public ArrayList<String> contentTypes; 053 public ArrayList<String> floorTypes; 054 public final int[][] content, floors; 055 056 /** 057 * Meant for generating large ArrayLists of Strings where an individual String may occur quite a few times. 058 * The rest parameter is a vararg (it may also be an Object array) of alternating String and Integer values, where 059 * an Integer is how many times to repeat the preceding String in the returned ArrayList. 060 * @param rest a vararg (or Object array) of alternating String and Integer values 061 * @return an ArrayList of Strings, probably with some or most of them repeated; you may want to shuffle this result 062 */ 063 public static ArrayList<String> makeRepeats(Object... rest) 064 { 065 if(rest == null || rest.length < 2) 066 { 067 return new ArrayList<>(0); 068 } 069 ArrayList<String> al = new ArrayList<>(rest.length); 070 071 for (int i = 0; i < rest.length - 1; i+=2) { 072 try { 073 int count = (int)rest[i+1]; 074 String v = (String) rest[i]; 075 for (int j = 0; j < count; j++) { 076 al.add(v); 077 } 078 }catch (ClassCastException ignored) { 079 } 080 } 081 return al; 082 } 083 public static ArrayList<String> makeShuffledRepeats(IRNG rng, Object... rest) 084 { 085 ArrayList<String> al = makeRepeats(rest); 086 rng.shuffleInPlace(al); 087 return al; 088 } 089 public static ArrayList<String> makeVegetation(IRNG rng, int size, double monoculture, FakeLanguageGen naming) 090 { 091 Thesaurus t = new Thesaurus(rng); 092 ArrayList<String> al = new ArrayList<>(size); 093 String latest; 094 for (int i = size; i > 0; i--) { 095 al.add(latest = t.makePlantName(naming)); 096 for (double j = rng.nextDouble(monoculture * 2.0 * size); j >= 1 && i > 0; j--, i--) { 097 al.add(latest); 098 } 099 } 100 rng.shuffleInPlace(al); 101 return al; 102 } 103 104 /** 105 * Gets a list of Strings that are really just the names of types of floor tile for wilderness areas. 106 * @param biome an index into {@link squidpony.squidgrid.mapping.WorldMapGenerator.DetailedBiomeMapper#biomeTable}, or some other index if you don't use DetailedBiomeMapper 107 * @param rng an IRNG, like {@link squidpony.squidmath.RNG} or {@link squidpony.squidmath.GWTRNG} 108 * @return a shuffled ArrayList that typically contains repeats of the kinds of floor that can appear here 109 */ 110 public static ArrayList<String> floorsByBiome(int biome, IRNG rng) { 111 biome &= 1023; 112 switch (biome) { 113 case 0: //Ice 114 case 1: 115 case 6: 116 case 12: 117 case 18: 118 case 24: 119 case 30: 120 case 42: 121 case 48: 122 return makeShuffledRepeats(rng, "snow", 3, "ice", 1); 123 case 7: //Tundra 124 case 13: 125 case 19: 126 case 25: 127 return makeShuffledRepeats(rng, "dirt", 6, "pebbles", 1, "snow", 9, "dry grass", 4); 128 case 26: //BorealForest 129 case 31: 130 case 32: 131 return makeShuffledRepeats(rng, "dirt", 3, "pebbles", 1, "snow", 11); 132 case 43: //River 133 case 44: 134 case 45: 135 case 46: 136 case 47: 137 case 49: 138 case 50: 139 case 51: 140 case 52: 141 case 53: 142 return makeList("fresh water"); 143 case 54: //Ocean 144 case 55: 145 case 56: 146 case 57: 147 case 58: 148 case 59: 149 return makeList("salt water"); 150 case 3: //Desert 151 case 4: 152 case 5: 153 case 10: 154 case 11: 155 case 17: 156 case 38: //Beach 157 case 39: 158 case 40: 159 case 41: 160 return makeList("sand"); 161 case 2: //Grassland 162 case 8: 163 case 9: 164 return makeShuffledRepeats(rng, "dirt", 8, "dry grass", 13, "grass", 2); 165 case 14: //Woodland 166 case 15: 167 return makeShuffledRepeats(rng, "dirt", 11, "leaves", 3, "dry grass", 8); 168 case 16: //Savanna 169 case 22: 170 case 23: 171 case 29: 172 return makeShuffledRepeats(rng, "dirt", 4, "dry grass", 17); 173 case 20: //SeasonalForest 174 case 21: 175 return makeShuffledRepeats(rng, "dirt", 9, "leaves", 6, "grass", 14); 176 case 27: //TemperateRainforest 177 case 33: 178 return makeShuffledRepeats(rng, "mud", 3, "leaves", 8, "grass", 10, "moss", 5); 179 case 28: //TropicalRainforest 180 case 34: 181 case 35: 182 return makeShuffledRepeats(rng, "mud", 7, "leaves", 6, "grass", 4, "moss", 11); 183 case 36: // Rocky 184 case 37: 185 return makeShuffledRepeats(rng, "pebbles", 5, "rubble", 1); 186 default: 187 return makeList("empty space"); 188 } 189 } 190 /** 191 * Gets a list of Strings that are really just the names of types of path tile for wilderness areas. 192 * Not currently used. 193 * @param biome an index into {@link squidpony.squidgrid.mapping.WorldMapGenerator.DetailedBiomeMapper#biomeTable}, or some other index if you don't use DetailedBiomeMapper 194 * @return an ArrayList that typically contains just the one or few types of path that can appear here 195 */ 196 public static ArrayList<String> pathsByBiome(int biome) { 197 biome &= 1023; 198 switch (biome) { 199 case 0: //Ice 200 case 1: 201 case 6: 202 case 12: 203 case 18: 204 case 24: 205 case 30: 206 case 42: 207 case 48: 208 return makeList("snow path"); 209 case 7: //Tundra 210 case 13: 211 case 19: 212 case 25: 213 case 26: //BorealForest 214 case 31: 215 case 32: 216 return makeList("snow path", "dirt path"); 217// case 43: //River 218// case 44: 219// case 45: 220// case 46: 221// case 47: 222// case 49: 223// case 50: 224// case 51: 225// case 52: 226// case 53: 227// case 54: //Ocean 228// case 55: 229// case 56: 230// case 57: 231// case 58: 232// case 59: 233// return makeList("wooden bridge"); 234 case 3: //Desert 235 case 4: 236 case 5: 237 case 10: 238 case 11: 239 case 17: 240 case 38: //Beach 241 case 39: 242 case 40: 243 case 41: 244 return makeList("sand path"); 245 case 2: //Grassland 246 case 8: 247 case 9: 248 case 14: //Woodland 249 case 15: 250 case 16: //Savanna 251 case 22: 252 case 23: 253 case 29: 254 return makeList("dirt path"); 255 case 20: //SeasonalForest 256 case 21: 257 return makeList("dirt path", "grass path"); 258 case 27: //TemperateRainforest 259 case 33: 260 case 28: //TropicalRainforest 261 case 34: 262 case 35: 263 return makeList("grass path"); 264 case 36: // Rocky 265 case 37: 266 return makeList("stone path"); 267 default: 268 return makeList("wooden bridge"); 269 270 } 271 } 272 /** 273 * Gets a list of Strings that are really just the names of types of terrain feature for wilderness areas. 274 * @param biome an index into {@link squidpony.squidgrid.mapping.WorldMapGenerator.DetailedBiomeMapper#biomeTable}, or some other index if you don't use DetailedBiomeMapper 275 * @param rng an IRNG, like {@link squidpony.squidmath.RNG} or {@link squidpony.squidmath.GWTRNG} 276 * @return a shuffled ArrayList that typically contains repeats of the kinds of terrain feature that can appear here 277 */ 278 public static ArrayList<String> contentByBiome(int biome, IRNG rng) { 279 biome &= 1023; 280 switch (biome) { 281 case 0: //Ice 282 case 1: 283 case 6: 284 case 12: 285 case 18: 286 case 24: 287 case 30: 288 case 42: 289 case 48: 290 return makeShuffledRepeats(rng, "snow mound", 5, "icy divot", 2, "powder snowdrift", 5); 291 case 7: //Tundra 292 case 13: 293 case 19: 294 case 25: 295 return makeShuffledRepeats(rng, "snow mound", 4, "hillock", 6, "animal burrow", 5, "small bush 1", 2); 296 case 26: //BorealForest 297 case 31: 298 case 32: 299 return makeShuffledRepeats(rng, "snow mound", 3, "small bush 1", 5, "large bush 1", 3, "evergreen tree 1", 17, "evergreen tree 2", 12); 300// case 43: //River 301// case 44: 302// case 45: 303// case 46: 304// case 47: 305// case 49: 306// case 50: 307// case 51: 308// case 52: 309// case 53: 310// case 54: //Ocean 311// case 55: 312// case 56: 313// case 57: 314// case 58: 315// case 59: 316// return new ArrayList<>(0); 317 case 3: //Desert 318 case 4: 319 case 5: 320 case 10: 321 case 11: 322 case 17: 323 return makeShuffledRepeats(rng, "small cactus 1", 2, "large cactus 1", 2, "succulent 1", 1, "animal burrow", 2); 324 case 38: //Beach 325 case 39: 326 case 40: 327 case 41: 328 return makeShuffledRepeats(rng, "seashell 1", 3, "seashell 2", 3, "seashell 3", 3, "seashell 4", 3, "driftwood", 5, "boulder", 3); 329 case 2: //Grassland 330 case 8: 331 case 9: 332 return makeShuffledRepeats(rng, "deciduous tree 1", 3, "small bush 1", 5, "small bush 2", 4, "large bush 1", 5, "animal burrow", 8, "hillock", 4); 333 case 14: //Woodland 334 case 15: 335 return makeShuffledRepeats(rng, "deciduous tree 1", 12, "deciduous tree 2", 9, "deciduous tree 3", 6, "small bush 1", 4, "small bush 2", 3, "animal burrow", 3); 336 case 16: //Savanna 337 case 22: 338 case 23: 339 case 29: 340 return makeShuffledRepeats(rng, "small bush 1", 8, "small bush 2", 5, "large bush 1", 2, "animal burrow", 3, "hillock", 6); 341 case 20: //SeasonalForest 342 case 21: 343 return makeShuffledRepeats(rng, "deciduous tree 1", 15, "deciduous tree 2", 13, "deciduous tree 3", 12, "small bush 1", 3, "large bush 1", 5, "large bush 2", 4, "animal burrow", 3); 344 case 27: //TemperateRainforest 345 case 33: 346 return makeShuffledRepeats(rng, "tropical tree 1", 6, "tropical tree 2", 5, "deciduous tree 1", 13, "deciduous tree 2", 12, "small bush 1", 8, "large bush 1", 7, "large bush 2", 7, "large bush 3", 3, "animal burrow", 3); 347 case 28: //TropicalRainforest 348 case 34: 349 case 35: 350 return makeShuffledRepeats(rng, "tropical tree 1", 12, "tropical tree 2", 11, "tropical tree 3", 10, "tropical tree 4", 9, "small bush 1", 6, "small bush 2", 5, "large bush 1", 6, "large bush 2", 5, "large bush 3", 3, "animal burrow", 9, "boulder", 1); 351 case 36: // Rocky 352 case 37: 353 return makeShuffledRepeats(rng, "seashell 1", 3, "seashell 2", 2, "seashell 3", 2, "driftwood", 6, "boulder", 9); 354 default: 355 return new ArrayList<>(0); 356 } 357 } 358// //COLDEST //COLDER //COLD //HOT //HOTTER //HOTTEST 359// "Ice", "Ice", "Grassland", "Desert", "Desert", "Desert", //DRYEST 360// "Ice", "Tundra", "Grassland", "Grassland", "Desert", "Desert", //DRYER 361// "Ice", "Tundra", "Woodland", "Woodland", "Savanna", "Desert", //DRY 362// "Ice", "Tundra", "SeasonalForest", "SeasonalForest", "Savanna", "Savanna", //WET 363// "Ice", "Tundra", "BorealForest", "TemperateRainforest", "TropicalRainforest", "Savanna", //WETTER 364// "Ice", "BorealForest", "BorealForest", "TemperateRainforest", "TropicalRainforest", "TropicalRainforest", //WETTEST 365// "Rocky", "Rocky", "Beach", "Beach", "Beach", "Beach", //COASTS 366// "Ice", "River", "River", "River", "River", "River", //RIVERS 367// "Ice", "River", "River", "River", "River", "River", //LAKES 368// "Ocean", "Ocean", "Ocean", "Ocean", "Ocean", "Ocean", //OCEAN 369// "Empty", //SPACE 370 371 public WildMap() 372 { 373 this(128, 128, 21); 374 } 375 public WildMap(int width, int height, int biome) 376 { 377 this(width, height, biome, new SilkRNG()); 378 } 379 public WildMap(int width, int height, int biome, int seedA, int seedB) 380 { 381 this(width, height, biome, new SilkRNG(seedA, seedB)); 382 } 383 public WildMap(int width, int height, int biome, IStatefulRNG rng) 384 { 385 this(width, height, biome, rng, floorsByBiome(biome, rng), contentByBiome(biome, rng)); 386 } 387 public WildMap(int width, int height, int biome, IStatefulRNG rng, ArrayList<String> contentTypes) 388 { 389 this(width, height, biome, rng, floorsByBiome(biome, rng), contentTypes); 390 } 391 public WildMap(int width, int height, int biome, IStatefulRNG rng, ArrayList<String> floorTypes, ArrayList<String> contentTypes) 392 { 393 this.width = width; 394 this.height = height; 395 this.biome = biome; 396 this.rng = rng; 397 content = ArrayTools.fill(-1, width, height); 398 floors = new int[width][height]; 399 this.floorTypes = floorTypes; 400 this.contentTypes = contentTypes; 401 } 402 403 /** 404 * Produces a map by filling the {@link #floors} 2D array with indices into {@link #floorTypes}, and similarly 405 * filling the {@link #content} 2D array with indices into {@link #contentTypes}. You only need to call this method 406 * when you first generate a map with the specific parameters you want, such as biome, and later if you want another 407 * map with the same parameters. 408 * <br> 409 * Virtually all of this method is a wrapper around functionality provided by {@link BlueNoise}, adjusted to fit 410 * wilderness maps slightly. 411 */ 412 public void generate() { 413 ArrayTools.fill(content, -1); 414 final int seed = rng.nextInt();//, otherSeed = rng.nextInt(), choice = seed + otherSeed & 15; 415 final int limit = contentTypes.size(), floorLimit = floorTypes.size(); 416 int b; 417 BlueNoise.blueSpill(floors, floorLimit, rng); 418 for (int x = 0; x < width; x++) { 419 for (int y = 0; y < height; y++) { 420 if((b = BlueNoise.getChosen(x, y, seed) + 128) < limit) 421 content[x][y] = b; 422 //floors[x][y] = (int)((FastNoise.instance.layered2D(x, y, otherSeed, 2, 0x1p-5f) * 0.4999f + 0.5f) * (floorLimit - 1) + 0.25f + rng.nextFloat(0.5f)); 423 } 424 } 425 } 426 427 /** 428 * A subclass of {@link WildMap} that serves as a ragged edge between 2, 3, or 4 WildMaps in a square intersection. 429 * You almost always supply 4 WildMaps to this (typically not other MixedWildMaps), one for each corner of the map, 430 * and this generates an uneven border between them. Make sure to look up the indices in the {@link #content} and 431 * {@link #floors} using this MixedWildMap's {@link #contentTypes} and {@link #floorTypes}, not the ones in the 432 * inner WildMaps, because the indices in the MixedWildMap are different. 433 */ 434 public static class MixedWildMap extends WildMap implements Serializable 435 { 436 private static final long serialVersionUID = 1L; 437 public final int[][] pieceMap; 438 public final WildMap[] pieces; 439 protected final int[] minFloors, maxFloors, minContents, maxContents; 440 441 public MixedWildMap() 442 { 443 this(new WildMap(), new WildMap(), new WildMap(), new WildMap(), new SilkRNG()); 444 } 445 446 public MixedWildMap(WildMap northeast, WildMap southeast, WildMap southwest, WildMap northwest, IStatefulRNG rng) 447 { 448 super(northeast.width, northeast.height, northeast.biome, rng, new ArrayList<>(northeast.floorTypes), new ArrayList<>(northeast.contentTypes)); 449 minFloors = new int[4]; 450 maxFloors = new int[4]; 451 minContents = new int[4]; 452 maxContents = new int[4]; 453 floorTypes.addAll(southeast.floorTypes); 454 floorTypes.addAll(southwest.floorTypes); 455 floorTypes.addAll(northwest.floorTypes); 456 contentTypes.addAll(southeast.contentTypes); 457 contentTypes.addAll(southwest.contentTypes); 458 contentTypes.addAll(northwest.contentTypes); 459 minFloors[1] = maxFloors[0] = northeast.floorTypes.size(); 460 minContents[1] = maxContents[0] = northeast.contentTypes.size(); 461 minFloors[2] = maxFloors[1] = maxFloors[0] + southeast.floorTypes.size(); 462 minContents[2] = maxContents[1] = maxContents[0] + southeast.contentTypes.size(); 463 minFloors[3] = maxFloors[2] = maxFloors[1] + southwest.floorTypes.size(); 464 minContents[3] = maxContents[2] = maxContents[1] + southwest.contentTypes.size(); 465 maxFloors[3] = maxFloors[2] + northwest.floorTypes.size(); 466 maxContents[3] = maxContents[2] + northwest.contentTypes.size(); 467 pieces = new WildMap[]{northeast, southeast, southwest, northwest}; 468 pieceMap = new int[width][height]; 469 } 470 471 protected void preparePieceMap() 472 { 473 ArrayTools.fill(pieceMap, 255); 474 pieceMap[width - 1][0] = 0; // northeast 475 pieceMap[width - 1][height - 1] = 1; // southeast 476 pieceMap[0][height - 1] = 2; // southwest 477 pieceMap[0][0] = 3; //northwest 478 final int spillerLimit = 4; 479 final Direction[] dirs = Direction.CARDINALS; 480 Direction d; 481 int t, rx, ry, ctr; 482 int[] ox = new int[width], oy = new int[height]; 483 boolean anySuccesses = false; 484 do { 485 ctr = 0; 486 rng.randomOrdering(width, ox); 487 rng.randomOrdering(height, oy); 488 for (int x = 0; x < width; x++) { 489 rx = ox[x]; 490 for (int y = 0; y < height; y++) { 491 ry = oy[y]; 492 if ((t = pieceMap[rx][ry]) < spillerLimit) { 493 d = dirs[rng.next(2)]; 494 if (rx + d.deltaX >= 0 && rx + d.deltaX < width && ry + d.deltaY >= 0 && ry + d.deltaY < height && 495 pieceMap[rx + d.deltaX][ry + d.deltaY] >= spillerLimit) { 496 pieceMap[rx + d.deltaX][ry + d.deltaY] = t; 497 ctr++; 498 } 499 d = dirs[rng.next(2)]; 500 if (rx + d.deltaX >= 0 && rx + d.deltaX < width && ry + d.deltaY >= 0 && ry + d.deltaY < height && 501 pieceMap[rx + d.deltaX][ry + d.deltaY] >= spillerLimit) { 502 pieceMap[rx + d.deltaX][ry + d.deltaY] = t; 503 ctr++; 504 } 505 } 506 507 } 508 } 509 if(!anySuccesses && ctr == 0) 510 { 511 ArrayTools.fill(pieceMap, 0); 512 return; 513 } 514 else 515 anySuccesses = true; 516 } while (ctr > 0); 517 do { 518 ctr = 0; 519 rng.randomOrdering(width, ox); 520 rng.randomOrdering(height, oy); 521 for (int x = 0; x < width; x++) { 522 rx = ox[x]; 523 for (int y = 0; y < height; y++) { 524 ry = oy[y]; 525 if ((t = pieceMap[rx][ry]) < spillerLimit) { 526 for (int i = 0; i < 4; i++) { 527 d = dirs[i]; 528 if (rx + d.deltaX >= 0 && rx + d.deltaX < width && ry + d.deltaY >= 0 && ry + d.deltaY < height && 529 pieceMap[rx + d.deltaX][ry + d.deltaY] >= spillerLimit) { 530 pieceMap[rx + d.deltaX][ry + d.deltaY] = t; 531 ctr++; 532 } 533 } 534 } 535 536 } 537 } 538 } while (ctr > 0); 539 540 } 541 542 @Override 543 public void generate() { 544 ArrayTools.fill(content, -1); 545 for (int i = 0; i < pieces.length; i++) { 546 pieces[i].generate(); 547 } 548 preparePieceMap(); 549 int p, c; 550 WildMap piece; 551 for (int x = 0; x < width; x++) { 552 for (int y = 0; y < height; y++) { 553 p = pieceMap[x][y]; 554 piece = pieces[p]; 555 floors[x][y] = piece.floors[x][y] + minFloors[p]; 556 if((c = piece.content[x][y]) >= 0) 557 { 558 content[x][y] = c + minContents[p]; 559 } 560 } 561 } 562 } 563 } 564}