001package squidpony.squidmath; 002 003import squidpony.squidgrid.Radius; 004 005import java.util.ArrayList; 006import java.util.Collections; 007import java.util.HashSet; 008 009/** 010 * This provides a Uniform Poisson Disk Sampling technique that can be used to generate random points that have a 011 * uniform minimum distance between each other. Due to Coord in SquidLib using ints and most Poisson Disk algorithms 012 * using floating-point numbers, some imprecision is to be expected from rounding to the nearest integers x and y. 013 * 014 * The algorithm is from the "Fast Poisson Disk Sampling in Arbitrary Dimensions" paper by Robert Bridson 015 * http://www.cs.ubc.ca/~rbridson/docs/bridson-siggraph07-poissondisk.pdf 016 * 017 * Adapted from C# by Renaud Bedard, which was adapted from Java source by Herman Tulleken 018 * http://theinstructionlimit.com/fast-uniform-poisson-disk-sampling-in-c 019 * Created by Tommy Ettinger on 10/20/2015. 020 */ 021public class PoissonDisk { 022 private static final float rootTwo = (float) Math.sqrt(2), 023 pi2 = (float) (Math.PI * 2.0); 024 025 private static final int defaultPointsPlaced = 10; 026 private static final Radius disk = Radius.CIRCLE; 027 028 private PoissonDisk() { 029 } 030 031 /** 032 * Get a list of Coords, each randomly positioned around the given center out to the given radius (measured with 033 * Euclidean distance, so a true circle), but with the given minimum distance from any other Coord in the list. 034 * The parameters maxX and maxY should typically correspond to the width and height of the map; no points will have 035 * positions with x equal to or greater than maxX and the same for y and maxY; similarly, no points will have 036 * negative x or y. 037 * @param center the center of the circle to spray Coords into 038 * @param radius the radius of the circle to spray Coords into 039 * @param minimumDistance the minimum distance between Coords, in Euclidean distance as a float. 040 * @param maxX one more than the highest x that can be assigned; typically an array length 041 * @param maxY one more than the highest y that can be assigned; typically an array length 042 * @return an ArrayList of Coord that satisfy the minimum distance; the length of the array can vary 043 */ 044 public static OrderedSet<Coord> sampleCircle(Coord center, float radius, float minimumDistance, 045 int maxX, int maxY) 046 { 047 return sampleCircle(center, radius, minimumDistance, maxX, maxY, defaultPointsPlaced, new StatefulRNG()); 048 } 049 050 /** 051 * Get a list of Coords, each randomly positioned around the given center out to the given radius (measured with 052 * Euclidean distance, so a true circle), but with the given minimum distance from any other Coord in the list. 053 * The parameters maxX and maxY should typically correspond to the width and height of the map; no points will have 054 * positions with x equal to or greater than maxX and the same for y and maxY; similarly, no points will have 055 * negative x or y. 056 * @param center the center of the circle to spray Coords into 057 * @param radius the radius of the circle to spray Coords into 058 * @param minimumDistance the minimum distance between Coords, in Euclidean distance as a float. 059 * @param maxX one more than the highest x that can be assigned; typically an array length 060 * @param maxY one more than the highest y that can be assigned; typically an array length 061 * @param pointsPerIteration with small radii, this can be around 5; with larger ones, 30 is reasonable 062 * @param rng an IRNG to use for all random sampling. 063 * @return an ArrayList of Coord that satisfy the minimum distance; the length of the array can vary 064 */ 065 public static OrderedSet<Coord> sampleCircle(Coord center, float radius, float minimumDistance, 066 int maxX, int maxY, int pointsPerIteration, IRNG rng) 067 { 068 int radius2 = Math.round(radius); 069 return sample(center.translate(-radius2, -radius2), center.translate(radius2, radius2), radius, minimumDistance, maxX, maxY, pointsPerIteration, rng); 070 } 071 072 /** 073 * Get a list of Coords, each randomly positioned within the rectangle between the given minPosition and 074 * maxPosition, but with the given minimum distance from any other Coord in the list. 075 * The parameters maxX and maxY should typically correspond to the width and height of the map; no points will have 076 * positions with x equal to or greater than maxX and the same for y and maxY; similarly, no points will have 077 * negative x or y. 078 * @param minPosition the Coord with the lowest x and lowest y to be used as a corner for the bounding box 079 * @param maxPosition the Coord with the highest x and highest y to be used as a corner for the bounding box 080 * @param minimumDistance the minimum distance between Coords, in Euclidean distance as a float. 081 * @param maxX one more than the highest x that can be assigned; typically an array length 082 * @param maxY one more than the highest y that can be assigned; typically an array length 083 * @return an ArrayList of Coord that satisfy the minimum distance; the length of the array can vary 084 */ 085 public static OrderedSet<Coord> sampleRectangle(Coord minPosition, Coord maxPosition, float minimumDistance, 086 int maxX, int maxY) 087 { 088 return sampleRectangle(minPosition, maxPosition, minimumDistance, maxX, maxY, defaultPointsPlaced, new StatefulRNG()); 089 } 090 091 /** 092 * Get a list of Coords, each randomly positioned within the rectangle between the given minPosition and 093 * maxPosition, but with the given minimum distance from any other Coord in the list. 094 * The parameters maxX and maxY should typically correspond to the width and height of the map; no points will have 095 * positions with x equal to or greater than maxX and the same for y and maxY; similarly, no points will have 096 * negative x or y. 097 * @param minPosition the Coord with the lowest x and lowest y to be used as a corner for the bounding box 098 * @param maxPosition the Coord with the highest x and highest y to be used as a corner for the bounding box 099 * @param minimumDistance the minimum distance between Coords, in Euclidean distance as a float. 100 * @param maxX one more than the highest x that can be assigned; typically an array length 101 * @param maxY one more than the highest y that can be assigned; typically an array length 102 * @param pointsPerIteration with small areas, this can be around 5; with larger ones, 30 is reasonable 103 * @param rng an IRNG to use for all random sampling. 104 * @return an ArrayList of Coord that satisfy the minimum distance; the length of the array can vary 105 */ 106 public static OrderedSet<Coord> sampleRectangle(Coord minPosition, Coord maxPosition, float minimumDistance, 107 int maxX, int maxY, int pointsPerIteration, IRNG rng) 108 { 109 return sample(minPosition, maxPosition, 0f, minimumDistance, maxX, maxY, pointsPerIteration, rng); 110 } 111 112 private static OrderedSet<Coord> sample(Coord minPosition, Coord maxPosition, float rejectionDistance, 113 float minimumDistance, int maxX, int maxY, int pointsPerIteration, IRNG rng) 114 { 115 116 Coord center = minPosition.average(maxPosition); 117 Coord dimensions = maxPosition.subtract(minPosition); 118 float cellSize = Math.max(minimumDistance / rootTwo, 0.25f); 119 int gridWidth = (int)(dimensions.x / cellSize) + 1; 120 int gridHeight = (int)(dimensions.y / cellSize) + 1; 121 Coord[][] grid = new Coord[gridWidth][gridHeight]; 122 ArrayList<Coord> activePoints = new ArrayList<>(); 123 OrderedSet<Coord> points = new OrderedSet<>(128); 124 125 //add first point 126 boolean added = false; 127 while (!added) 128 { 129 float d = rng.nextFloat(); 130 int xr = Math.round(minPosition.x + dimensions.x * d); 131 132 d = rng.nextFloat(); 133 int yr = Math.round(minPosition.y + dimensions.y * d); 134 135 if (rejectionDistance > 0 && disk.radius(center.x, center.y, xr, yr) > rejectionDistance) 136 continue; 137 added = true; 138 Coord p = Coord.get(Math.min(xr, maxX - 1), Math.min(yr, maxY - 1)); 139 Coord index = p.subtract(minPosition).divide(cellSize); 140 141 grid[index.x][index.y] = p; 142 143 activePoints.add(p); 144 points.add(p); 145 } 146 //end add first point 147 148 while (activePoints.size() != 0) 149 { 150 int listIndex = rng.nextInt(activePoints.size()); 151 152 Coord point = activePoints.get(listIndex); 153 boolean found = false; 154 155 for (int k = 0; k < pointsPerIteration; k++) 156 { 157 //add next point 158 //get random point around 159 float d = rng.nextFloat(); 160 float radius = minimumDistance + minimumDistance * d; 161 float angle = pi2 * rng.nextFloat(); 162 163 float newX = radius * NumberTools.sin(angle); 164 float newY = radius * NumberTools.cos(angle); 165 Coord q = point.translateCapped(Math.round(newX), Math.round(newY), maxX, maxY); 166 //end get random point around 167 168 if (q.x >= minPosition.x && q.x <= maxPosition.x && 169 q.y >= minPosition.y && q.y <= maxPosition.y && 170 (rejectionDistance <= 0 || disk.radius(center.x, center.y, q.x, q.y) <= rejectionDistance)) 171 { 172 Coord qIndex = q.subtract(minPosition).divide((int)Math.ceil(cellSize)); 173 boolean tooClose = false; 174 175 for (int i = Math.max(0, qIndex.x - 2); i < Math.min(gridWidth, qIndex.x + 3) && !tooClose; i++) { 176 for (int j = Math.max(0, qIndex.y - 2); j < Math.min(gridHeight, qIndex.y + 3); j++) { 177 if (grid[i][j] != null && disk.radius(grid[i][j], q) < minimumDistance) { 178 tooClose = true; 179 break; 180 } 181 } 182 } 183 if (!tooClose) 184 { 185 found = true; 186 activePoints.add(q); 187 points.add(q); 188 grid[qIndex.x][qIndex.y] = q; 189 } 190 } 191 //end add next point 192 } 193 194 if (!found) 195 { 196 activePoints.remove(listIndex); 197 } 198 } 199 return points; 200 } 201 202 public static OrderedSet<Coord> sampleMap(char[][] map, 203 float minimumDistance, IRNG rng, Character... blocking) 204 { 205 return sampleMap(Coord.get(1, 1), Coord.get(map.length - 2, map[0].length - 2), 206 map, minimumDistance, rng, blocking); 207 } 208 209 public static OrderedSet<Coord> sampleMap(Coord minPosition, Coord maxPosition, char[][] map, 210 float minimumDistance, IRNG rng, Character... blocking) { 211 int width = map.length; 212 int height = map[0].length; 213 HashSet<Character> blocked = new HashSet<>(); 214 Collections.addAll(blocked, blocking); 215 boolean restricted = false; 216 if (blocked.size() > 0) { 217 restricted = true; 218 } 219 Coord dimensions = maxPosition.subtract(minPosition); 220 float cellSize = Math.max(minimumDistance / rootTwo, 1f); 221 int gridWidth = (int) (dimensions.x / cellSize) + 1; 222 int gridHeight = (int) (dimensions.y / cellSize) + 1; 223 Coord[][] grid = new Coord[gridWidth][gridHeight]; 224 ArrayList<Coord> activePoints = new ArrayList<>(); 225 OrderedSet<Coord> points = new OrderedSet<>(128); 226 227 //add first point 228 229 Coord p = randomUnblockedTile(minPosition, maxPosition, map, rng, blocked); 230 if (p == null) 231 return points; 232 Coord index = p.subtract(minPosition).divide(cellSize); 233 234 grid[index.x][index.y] = p; 235 236 activePoints.add(p); 237 points.add(p); 238 239 //end add first point 240 241 while (activePoints.size() != 0) { 242 int listIndex = rng.nextInt(activePoints.size()); 243 244 Coord point = activePoints.get(listIndex); 245 boolean found = false; 246 247 for (int k = 0; k < 20; k++) { 248 //add next point 249 //get random point around 250 float d = rng.nextFloat(); 251 float radius = minimumDistance + minimumDistance * d; 252 d = rng.nextFloat(); 253 float angle = pi2 * d; 254 255 float newX = radius * NumberTools.sin(angle); 256 float newY = radius * NumberTools.cos(angle); 257 Coord q = point.translateCapped(Math.round(newX), Math.round(newY), width, height); 258 int frustration = 0; 259 while(restricted && blocked.contains(map[q.x][q.y]) && frustration < 8) 260 { 261 d = rng.nextFloat(); 262 angle = pi2 * d; 263 newX = radius * NumberTools.sin(angle); 264 newY = radius * NumberTools.cos(angle); 265 q = point.translateCapped(Math.round(newX), Math.round(newY), width, height); 266 frustration++; 267 } 268 269 //end get random point around 270 271 if (q.x >= minPosition.x && q.x <= maxPosition.x && 272 q.y >= minPosition.y && q.y <= maxPosition.y) { 273 Coord qIndex = q.subtract(minPosition).divide((int) Math.ceil(cellSize)); 274 boolean tooClose = false; 275 276 for (int i = Math.max(0, qIndex.x - 2); i < Math.min(gridWidth, qIndex.x + 3) && !tooClose; i++) { 277 for (int j = Math.max(0, qIndex.y - 2); j < Math.min(gridHeight, qIndex.y + 3); j++) { 278 if (grid[i][j] != null && disk.radius(grid[i][j], q) < minimumDistance) { 279 tooClose = true; 280 break; 281 } 282 } 283 } 284 if (!tooClose) { 285 found = true; 286 activePoints.add(q); 287 if(!restricted || !blocked.contains(map[q.x][q.y])) 288 points.add(q); 289 grid[qIndex.x][qIndex.y] = q; 290 } 291 } 292 //end add next point 293 } 294 295 if (!found) 296 activePoints.remove(listIndex); 297 } 298 return points; 299 } 300 /** 301 * 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. 302 * Uses the given IRNG for pseudo-random number generation. 303 * @param minPosition the Coord with the lowest x and lowest y to be used as a corner for the bounding box 304 * @param maxPosition the Coord with the highest x and highest y to be used as a corner for the bounding box 305 * @param map a dungeon map or something, x then y 306 * @param rng a IRNG to generate random choices 307 * @param blocked a Set of Characters that block a tile from being chosen 308 * @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. 309 */ 310 public static Coord randomUnblockedTile(Coord minPosition, Coord maxPosition, char[][] map, IRNG rng, HashSet<Character> blocked) 311 { 312 int width = map.length; 313 int height = map[0].length; 314 if(width < 3 || height < 3) 315 return null; 316 if(blocked.size() == 0) { 317 return Coord.get(rng.between(minPosition.x, maxPosition.x), rng.between(minPosition.y, maxPosition.y)); 318 } 319 320 int x = rng.between(minPosition.x, maxPosition.x), y = rng.between(minPosition.y, maxPosition.y); 321 for(int i = 0; i < (width + height) / 4; i++) 322 { 323 if(!blocked.contains(map[x][y])) 324 { 325 return Coord.get(x, y); 326 } 327 else 328 { 329 x = rng.between(minPosition.x, maxPosition.x); 330 y = rng.between(minPosition.y, maxPosition.y); 331 } 332 } 333 x = 1; 334 y = 1; 335 if(!blocked.contains(map[x][y])) 336 return Coord.get(x, y); 337 338 while(blocked.contains(map[x][y])) 339 { 340 x += 1; 341 if(x >= width - 1) 342 { 343 x = 1; 344 y += 1; 345 } 346 if(y >= height - 1) 347 return null; 348 } 349 return Coord.get(x, y); 350 } 351 352}