package squidpony.squidmath; import squidpony.squidgrid.Radius; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; /** * This provides a Uniform Poisson Disk Sampling technique that can be used to generate random points that have a * uniform minimum distance between each other. Due to Coord in SquidLib using ints and most Poisson Disk algorithms * using floating-point numbers, some imprecision is to be expected from rounding to the nearest integers x and y. * * The algorithm is from the "Fast Poisson Disk Sampling in Arbitrary Dimensions" paper by Robert Bridson * http://www.cs.ubc.ca/~rbridson/docs/bridson-siggraph07-poissondisk.pdf * * Adapted from C# by Renaud Bedard, which was adapted from Java source by Herman Tulleken * http://theinstructionlimit.com/fast-uniform-poisson-disk-sampling-in-c * Created by Tommy Ettinger on 10/20/2015. */ public class PoissonDisk { private static final float rootTwo = (float) Math.sqrt(2), pi = (float) Math.PI, pi2 = pi * 2f, halfPi = pi * 0.5f; private static final int defaultPointsPlaced = 10; private static final Radius disk = Radius.CIRCLE; private PoissonDisk() { } /** * Get a list of Coords, each randomly positioned around the given center out to the given radius (measured with * Euclidean distance, so a true circle), but with the given minimum distance from any other Coord in the list. * The parameters maxX and maxY should typically correspond to the width and height of the map; no points will have * positions with x equal to or greater than maxX and the same for y and maxY; similarly, no points will have * negative x or y. * @param center the center of the circle to spray Coords into * @param radius the radius of the circle to spray Coords into * @param minimumDistance the minimum distance between Coords, in Euclidean distance as a float. * @param maxX one more than the highest x that can be assigned; typically an array length * @param maxY one more than the highest y that can be assigned; typically an array length * @return an ArrayList of Coord that satisfy the minimum distance; the length of the array can vary */ public static ArrayList<Coord> sampleCircle(Coord center, float radius, float minimumDistance, int maxX, int maxY) { return sampleCircle(center, radius, minimumDistance, maxX, maxY, defaultPointsPlaced, new StatefulRNG()); } /** * Get a list of Coords, each randomly positioned around the given center out to the given radius (measured with * Euclidean distance, so a true circle), but with the given minimum distance from any other Coord in the list. * The parameters maxX and maxY should typically correspond to the width and height of the map; no points will have * positions with x equal to or greater than maxX and the same for y and maxY; similarly, no points will have * negative x or y. * @param center the center of the circle to spray Coords into * @param radius the radius of the circle to spray Coords into * @param minimumDistance the minimum distance between Coords, in Euclidean distance as a float. * @param maxX one more than the highest x that can be assigned; typically an array length * @param maxY one more than the highest y that can be assigned; typically an array length * @param pointsPerIteration with small radii, this can be around 5; with larger ones, 30 is reasonable * @param rng an RNG to use for all random sampling. * @return an ArrayList of Coord that satisfy the minimum distance; the length of the array can vary */ public static ArrayList<Coord> sampleCircle(Coord center, float radius, float minimumDistance, int maxX, int maxY, int pointsPerIteration, RNG rng) { int radius2 = Math.round(radius); return sample(center.translate(-radius2, -radius2), center.translate(radius2, radius2), radius, minimumDistance, maxX, maxY, pointsPerIteration, rng); } /** * Get a list of Coords, each randomly positioned within the rectangle between the given minPosition and * maxPosition, but with the given minimum distance from any other Coord in the list. * The parameters maxX and maxY should typically correspond to the width and height of the map; no points will have * positions with x equal to or greater than maxX and the same for y and maxY; similarly, no points will have * negative x or y. * @param minPosition the Coord with the lowest x and lowest y to be used as a corner for the bounding box * @param maxPosition the Coord with the highest x and highest y to be used as a corner for the bounding box * @param minimumDistance the minimum distance between Coords, in Euclidean distance as a float. * @param maxX one more than the highest x that can be assigned; typically an array length * @param maxY one more than the highest y that can be assigned; typically an array length * @return an ArrayList of Coord that satisfy the minimum distance; the length of the array can vary */ public static ArrayList<Coord> sampleRectangle(Coord minPosition, Coord maxPosition, float minimumDistance, int maxX, int maxY) { return sampleRectangle(minPosition, maxPosition, minimumDistance, maxX, maxY, defaultPointsPlaced, new StatefulRNG()); } /** * Get a list of Coords, each randomly positioned within the rectangle between the given minPosition and * maxPosition, but with the given minimum distance from any other Coord in the list. * The parameters maxX and maxY should typically correspond to the width and height of the map; no points will have * positions with x equal to or greater than maxX and the same for y and maxY; similarly, no points will have * negative x or y. * @param minPosition the Coord with the lowest x and lowest y to be used as a corner for the bounding box * @param maxPosition the Coord with the highest x and highest y to be used as a corner for the bounding box * @param minimumDistance the minimum distance between Coords, in Euclidean distance as a float. * @param maxX one more than the highest x that can be assigned; typically an array length * @param maxY one more than the highest y that can be assigned; typically an array length * @param pointsPerIteration with small areas, this can be around 5; with larger ones, 30 is reasonable * @param rng an RNG to use for all random sampling. * @return an ArrayList of Coord that satisfy the minimum distance; the length of the array can vary */ public static ArrayList<Coord> sampleRectangle(Coord minPosition, Coord maxPosition, float minimumDistance, int maxX, int maxY, int pointsPerIteration, RNG rng) { return sample(minPosition, maxPosition, 0f, minimumDistance, maxX, maxY, pointsPerIteration, rng); } private static ArrayList<Coord> sample(Coord minPosition, Coord maxPosition, float rejectionDistance, float minimumDistance, int maxX, int maxY, int pointsPerIteration, RNG rng) { Coord center = minPosition.average(maxPosition); Coord dimensions = maxPosition.subtract(minPosition); float cellSize = Math.max(minimumDistance / rootTwo, 0.25f); int gridWidth = (int)(dimensions.x / cellSize) + 1; int gridHeight = (int)(dimensions.y / cellSize) + 1; Coord[][] grid = new Coord[gridWidth][gridHeight]; ArrayList<Coord> activePoints = new ArrayList<>(); OrderedSet<Coord> points = new OrderedSet<>(128); //add first point boolean added = false; while (!added) { float d = rng.nextFloat(); int xr = Math.round(minPosition.x + dimensions.x * d); d = rng.nextFloat(); int yr = Math.round(minPosition.y + dimensions.y * d); if (rejectionDistance > 0 && disk.radius(center.x, center.y, xr, yr) > rejectionDistance) continue; added = true; Coord p = Coord.get(Math.min(xr, maxX - 1), Math.min(yr, maxY - 1)); Coord index = p.subtract(minPosition).divide(cellSize); grid[index.x][index.y] = p; activePoints.add(p); points.add(p); } //end add first point while (activePoints.size() != 0) { int listIndex = rng.nextInt(activePoints.size()); Coord point = activePoints.get(listIndex); boolean found = false; for (int k = 0; k < pointsPerIteration; k++) { //add next point //get random point around float d = rng.nextFloat(); float radius = minimumDistance + minimumDistance * d; d = rng.nextFloat(); float angle = pi2 * d; float newX = radius * (float)Math.sin(angle); float newY = radius * (float)Math.cos(angle); Coord q = point.translateCapped(Math.round(newX), Math.round(newY), maxX, maxY); //end get random point around if (q.x >= minPosition.x && q.x <= maxPosition.x && q.y >= minPosition.y && q.y <= maxPosition.y && (rejectionDistance <= 0 || disk.radius(center.x, center.y, q.x, q.y) <= rejectionDistance)) { Coord qIndex = q.subtract(minPosition).divide((int)Math.ceil(cellSize)); boolean tooClose = false; for (int i = Math.max(0, qIndex.x - 2); i < Math.min(gridWidth, qIndex.x + 3) && !tooClose; i++) { for (int j = Math.max(0, qIndex.y - 2); j < Math.min(gridHeight, qIndex.y + 3); j++) { if (grid[i][j] != null && disk.radius(grid[i][j], q) < minimumDistance) { tooClose = true; break; } } } if (!tooClose) { found = true; activePoints.add(q); points.add(q); grid[qIndex.x][qIndex.y] = q; } } //end add next point } if (!found) { activePoints.remove(listIndex); } } activePoints = new ArrayList<>(points); return activePoints; } public static ArrayList<Coord> sampleMap(char[][] map, float minimumDistance, RNG rng, Character... blocking) { return sampleMap(Coord.get(1, 1), Coord.get(map.length - 2, map[0].length - 2), map, minimumDistance, rng, blocking); } public static ArrayList<Coord> sampleMap(Coord minPosition, Coord maxPosition, char[][] map, float minimumDistance, RNG rng, Character... blocking) { int width = map.length; int height = map[0].length; HashSet<Character> blocked = new HashSet<>(); Collections.addAll(blocked, blocking); boolean restricted = false; if (blocked.size() > 0) { restricted = true; } Coord dimensions = maxPosition.subtract(minPosition); float cellSize = Math.max(minimumDistance / rootTwo, 1f); int gridWidth = (int) (dimensions.x / cellSize) + 1; int gridHeight = (int) (dimensions.y / cellSize) + 1; Coord[][] grid = new Coord[gridWidth][gridHeight]; ArrayList<Coord> activePoints = new ArrayList<>(); OrderedSet<Coord> points = new OrderedSet<>(128); //add first point Coord p = randomUnblockedTile(minPosition, maxPosition, map, rng, blocked); if (p == null) return activePoints; Coord index = p.subtract(minPosition).divide(cellSize); grid[index.x][index.y] = p; activePoints.add(p); points.add(p); //end add first point while (activePoints.size() != 0) { int listIndex = rng.nextInt(activePoints.size()); Coord point = activePoints.get(listIndex); boolean found = false; for (int k = 0; k < 20; k++) { //add next point //get random point around float d = rng.nextFloat(); float radius = minimumDistance + minimumDistance * d; d = rng.nextFloat(); float angle = pi2 * d; float newX = radius * (float) Math.sin(angle); float newY = radius * (float) Math.cos(angle); Coord q = point.translateCapped(Math.round(newX), Math.round(newY), width, height); int frustration = 0; while(blocked.contains(map[q.x][q.y]) && frustration < 8) { d = rng.nextFloat(); angle = pi2 * d; newX = radius * (float) Math.sin(angle); newY = radius * (float) Math.cos(angle); q = point.translateCapped(Math.round(newX), Math.round(newY), width, height); frustration++; } //end get random point around if (q.x >= minPosition.x && q.x <= maxPosition.x && q.y >= minPosition.y && q.y <= maxPosition.y) { Coord qIndex = q.subtract(minPosition).divide((int) Math.ceil(cellSize)); boolean tooClose = false; for (int i = Math.max(0, qIndex.x - 2); i < Math.min(gridWidth, qIndex.x + 3) && !tooClose; i++) { for (int j = Math.max(0, qIndex.y - 2); j < Math.min(gridHeight, qIndex.y + 3); j++) { if (grid[i][j] != null && disk.radius(grid[i][j], q) < minimumDistance) { tooClose = true; break; } } } if (!tooClose) { found = true; activePoints.add(q); if(!blocked.contains(map[q.x][q.y])) points.add(q); grid[qIndex.x][qIndex.y] = q; } } //end add next point } if (!found) activePoints.remove(listIndex); } activePoints = new ArrayList<>(points); return activePoints; } /** * Finds a random Coord where the x and y match up to a [x][y] location on map that has any value not in blocking. * Uses the given RNG for pseudo-random number generation. * @param minPosition the Coord with the lowest x and lowest y to be used as a corner for the bounding box * @param maxPosition the Coord with the highest x and highest y to be used as a corner for the bounding box * @param map a dungeon map or something, x then y * @param rng a RNG to generate random choices * @param blocked a Set of Characters that block a tile from being chosen * @return a Coord that corresponds to a map element equal to tile, or null if tile cannot be found or if map is too small. */ public static Coord randomUnblockedTile(Coord minPosition, Coord maxPosition, char[][] map, RNG rng, HashSet<Character> blocked) { int width = map.length; int height = map[0].length; if(width < 3 || height < 3) return null; if(blocked.size() == 0) { return Coord.get(rng.between(minPosition.x, maxPosition.x), rng.between(minPosition.y, maxPosition.y)); } int x = rng.between(minPosition.x, maxPosition.x), y = rng.between(minPosition.y, maxPosition.y); for(int i = 0; i < (width + height) / 4; i++) { if(!blocked.contains(map[x][y])) { return Coord.get(x, y); } else { x = rng.between(minPosition.x, maxPosition.x); y = rng.between(minPosition.y, maxPosition.y); } } x = 1; y = 1; if(!blocked.contains(map[x][y])) return Coord.get(x, y); while(blocked.contains(map[x][y])) { x += 1; if(x >= width - 1) { x = 1; y += 1; } if(y >= height - 1) return null; } return Coord.get(x, y); } }