001package squidpony.squidgrid.mapping; 002 003 004import squidpony.ArrayTools; 005import squidpony.squidgrid.Direction; 006import squidpony.squidmath.Coord; 007import squidpony.squidmath.GWTRNG; 008import squidpony.squidmath.IRNG; 009import squidpony.squidmath.OrderedSet; 010 011/** 012 * A maze generator that can be configured using a {@link ChoosingMethod}, which can be customized for the app. 013 * Based in part on code from <a href="http://weblog.jamisbuck.org/2011/1/27/maze-generation-growing-tree-algorithm">Jamis Buck's blog</a>. 014 * @author Eben Howard - http://squidpony.com - howard@squidpony.com 015 */ 016public class GrowingTreeMazeGenerator implements IDungeonGenerator { 017 018 private IRNG rng; 019 private int width, height; 020 public char[][] dungeon; 021 022 public GrowingTreeMazeGenerator(int width, int height) { 023 this.width = width; 024 this.height = height; 025 rng = new GWTRNG(); 026 } 027 public GrowingTreeMazeGenerator(int width, int height, IRNG rng) { 028 this.width = width; 029 this.height = height; 030 this.rng = rng; 031 } 032 033 /** 034 * Gets the most recently-produced dungeon as a 2D char array, usually produced by calling {@link #generate()} or 035 * some similar method present in a specific implementation. This normally passes a direct reference and not a copy, 036 * so you can normally modify the returned array to propagate changes back into this IDungeonGenerator. 037 * 038 * @return the most recently-produced dungeon/map as a 2D char array 039 */ 040 @Override 041 public char[][] getDungeon() { 042 return dungeon; 043 } 044 045 /** 046 * Builds and returns a 2D char array maze by using {@link #newest} with {@link #generate(ChoosingMethod)}. 047 * 048 * @return {@link #dungeon}, after filling it with a maze 049 */ 050 @Override 051 public char[][] generate() { 052 return generate(newest); 053 } 054 055 /** 056 * Builds and returns a 2D char array maze using the provided chooser method object. The most maze-like dungeons 057 * use {@link #newest}, the least maze-like use {@link #oldest}, and the most jumbled use {@link #random} or a 058 * mix of others using {@link #mix(ChoosingMethod, double, ChoosingMethod, double)}. 059 * 060 * @param choosing the callback object for making the split decision 061 * @return {@link #dungeon}, after filling it with a maze 062 */ 063 public char[][] generate(ChoosingMethod choosing) { 064 if(dungeon == null || dungeon.length != width || dungeon[0].length != height) 065 dungeon = ArrayTools.fill('#', width, height); 066 else 067 ArrayTools.fill(dungeon, '#'); 068 069 int x = rng.nextInt(width - 1) | 1; 070 int y = rng.nextInt(height - 1) | 1; 071 072 OrderedSet<Coord> deck = new OrderedSet<>(); 073 deck.add(Coord.get(x, y)); 074 075 Direction[] dirs = new Direction[4]; 076 System.arraycopy(Direction.CARDINALS, 0, dirs, 0, 4); 077 OUTER: 078 while (!deck.isEmpty()) { 079 int i = choosing.chooseIndex(deck.size()); 080 Coord p = deck.getAt(i); 081 rng.shuffleInPlace(dirs); 082 083 for (Direction dir : dirs) { 084 x = p.x + dir.deltaX * 2; 085 y = p.y + dir.deltaY * 2; 086 if (x > 0 && x < width - 1 && y > 0 && y < height - 1) { 087 if (dungeon[x][y] == '#' && deck.add(Coord.get(x, y))) { 088 dungeon[x][y] = '.'; 089 dungeon[p.x + dir.deltaX][p.y + dir.deltaY] = '.'; 090 continue OUTER; 091 } 092 } 093 } 094 095 deck.remove(p); 096 } 097 098 return dungeon; 099 } 100 101 /** 102 * A way to configure how {@link #generate(ChoosingMethod)} places paths through the maze. Most often, you'll want 103 * to at least start with {@link #newest}, since it generates the most maze-like dungeons, and try the other 104 * predefined ChoosingMethods eventually. You can use {@link #mix(ChoosingMethod, double, ChoosingMethod, double)} 105 * to mix two ChoosingMethods, which can be a good way to add a little variety to a maze. 106 */ 107 public interface ChoosingMethod { 108 109 /** 110 * Given the size to choose from, will return a single value smaller than the passed in value and greater than 111 * or equal to 0. The value chosen is dependent on the individual implementation. 112 * 113 * @param size the exclusive upper bound for results; always at least 1 114 * @return an int between 0 (inclusive) and {@code size} (exclusive) 115 */ 116 int chooseIndex(int size); 117 } 118 119 /** 120 * Produces high-quality mazes that are very similar to those produced by a recursive back-tracking algorithm. 121 */ 122 public final ChoosingMethod newest = new ChoosingMethod() { 123 @Override 124 public int chooseIndex(int size) { 125 return size - 1; 126 } 127 }; 128 /** 129 * Produces mostly straight corridors that dead-end at the map's edge; probably only useful with {@link #mix(ChoosingMethod, double, ChoosingMethod, double)}. 130 */ 131 public final ChoosingMethod oldest = new ChoosingMethod() { 132 @Override 133 public int chooseIndex(int size) { 134 return 0; 135 } 136 }; 137 /** 138 * Produces chaotic, jumbled spans of corridors that are similar to those produced by Prim's algorithm. 139 */ 140 public final ChoosingMethod random = new ChoosingMethod() { 141 @Override 142 public int chooseIndex(int size) { 143 return rng.nextSignedInt(size); 144 } 145 }; 146 147 /** 148 * Mixes two ChoosingMethod values, like {@link #newest} and {@link #random}, given a weight for each, and produces 149 * a new ChoosingMethod that randomly (respecting weight) picks one of those ChoosingMethods each time it is used. 150 * @param methodA the first ChoosingMethod to mix; must not be null 151 * @param chanceA the weight to favor choosing methodA 152 * @param methodB the second ChoosingMethod to mix; must not be null 153 * @param chanceB the weight to favor choosing methodB 154 * @return a ChoosingMethod that randomly picks between {@code methodA} and {@code methodB} each time it is used 155 */ 156 public ChoosingMethod mix(final ChoosingMethod methodA, final double chanceA, 157 final ChoosingMethod methodB, final double chanceB) { 158 final double a = Math.max(0.0, chanceA); 159 final double sum = a + Math.max(0.0, chanceB); 160 if(sum <= 0.0) return random; 161 return new ChoosingMethod() { 162 @Override 163 public int chooseIndex(int size) { 164 return rng.nextDouble(sum) < a ? methodA.chooseIndex(size) : methodB.chooseIndex(size); 165 } 166 }; 167 } 168}