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}