package squidpony.squidgrid; import squidpony.squidgrid.mapping.DungeonUtility; import squidpony.squidmath.Coord; import squidpony.squidmath.RNG; import java.util.*; /** * A alternative to {@link Spill}, whose purpose is to have a simpler API. You * can specify the characters that are impassable (in other words: that should * not be spilled on) using {@link #addImpassableChar(char)} and * {@link #removeImpassableChar(char)}. By default the set of impassable characters * is {@code '#'}. * * @author smelC * * @see Spill An alternative implementation of spilling. */ public class Splash { private static Splash splashCache = null; private static int splashHash = -1; protected final Set<Character> impassable; /** * A fresh instance, whose only impassable character is '#'. */ public Splash() { this.impassable = new HashSet<>(); /* The default */ addImpassableChar('#'); } /** * A fresh instance, adding the chars in blocked to the set of impassable characters, * then also adding '#' if it isn't present. You can remove '#' with * {@link #removeImpassableChar(char)} if you use '#' to mean something non-blocking. */ public Splash(Set<Character> blocked) { this.impassable = new HashSet<>(blocked); /* The default */ addImpassableChar('#'); } /** * Adds {@code c} to the set of impassable characters. * * @param c * The character to add. */ public void addImpassableChar(char c) { this.impassable.add(c); } /** * Removes {@code c} from the set of impassable characters. * * @param c * The character to remove. * @return Whether it was in there. */ public boolean removeImpassableChar(char c) { return this.impassable.remove(c); } /** * @param rng used to randomize the floodfill * @param level char 2D array with x, y indices for the dungeon/map level * @param start * Where the spill should start. It should be passable, otherwise * an empty list gets returned. Consider using * {@link DungeonUtility#getRandomCell(RNG, char[][], Set, int)} * to find it. * @param volume * The number of cells to spill on. * @param drunks * The ratio of drunks to use to make the splash more realistic. * Like for dungeon generation, if greater than 0, drunk walkers * will remove the splash's margins, to make it more realistic. * You don't need that if you're doing a splash that is bounded * by walls, because the fill will be realistic. If you're doing * a splash that isn't bounded, use that for its borders not to * be too square. * * <p> * Useful values are 0, 1, and 2. Giving more will likely yield * an empty result, on any decent map sizes. * </p> * @return The spill. It is a list of coordinates (containing {@code start}) * valid in {@code level} that are all adjacent and whose symbol is * passable. If non-empty, this is guaranteed to be an * {@link ArrayList}. */ public List<Coord> spill(RNG rng, char[][] level, Coord start, int volume, int drunks) { if (!DungeonUtility.inLevel(level, start) || !passable(level[start.x][start.y])) return Collections.emptyList(); final List<Coord> result = new ArrayList<>(volume); Direction[] dirs = new Direction[Direction.OUTWARDS.length]; final LinkedList<Coord> toTry = new LinkedList<>(); toTry.add(start); final Set<Coord> trieds = new HashSet<>(); while (!toTry.isEmpty()) { assert result.size() < volume; final Coord current = toTry.removeFirst(); assert DungeonUtility.inLevel(level, current); assert passable(level[current.x][current.y]); if (trieds.contains(current)) continue; trieds.add(current); /* * Here it holds that either 'current == start' or there's a Coord * in 'result' that is adjacent to 'current'. */ result.add(current); if (result.size() == volume) /* We're done */ break; /* Now prepare data for next iterations */ /* Randomize directions */ dirs = rng.shuffle(Direction.OUTWARDS, dirs); for (Direction d : dirs) { final Coord next = current.translate(d); if (DungeonUtility.inLevel(level, next) && !trieds.contains(next) && passable(level[next.x][next.y])) /* A valid cell for trying to be spilled on */ toTry.add(next); } } if (0 < drunks) drunkinize(rng, level, result, DungeonUtility.border(result, null), drunks); return result; } /** * @param rng * @param map * The map on which {@code zone} is a pool * @param zone * The zone to shrink * @param border * {@code zone}'s border * @param drunks * The number of drunken walkers to consider */ protected void drunkinize(RNG rng, char[][] map, List<Coord> zone, List<Coord> border, int drunks) { if (drunks == 0) return; final int sz = zone.size(); final int nb = (sz / 10) * drunks; if (nb == 0) return; assert !border.isEmpty(); for (int j = 0; j < nb && !zone.isEmpty(); j++) { drunkinize0(rng, zone, border, drunks); if (border.isEmpty() || zone.isEmpty()) return; } } protected boolean passable(char c) { return !impassable.contains(c); } /** * Removes a circle from {@code zone}, by taking the circle's center in * {@code zone} 's border: {@code border}. * * @param border * {@code result}'s border. */ private void drunkinize0(RNG rng, List<Coord> zone, List<Coord> border, int nb) { assert !border.isEmpty(); assert !zone.isEmpty(); final int width = rng.nextInt(nb) + 1; final int height = rng.nextInt(nb) + 1; final int radius = Math.max(1, Math.round(nb * Math.min(width, height))); final Coord center = rng.getRandomElement(border); zone.remove(center); for (int dx = -radius; dx <= radius; ++dx) { final int high = (int) Math.floor(Math.sqrt(radius * radius - dx * dx)); for (int dy = -high; dy <= high; ++dy) { final Coord c = center.translate(dx, dy); zone.remove(c); if (zone.isEmpty()) return; } } } /** * @param rng * used to randomize the floodfill * @param level * char 2D array with x, y indices for the dungeon/map level * @param start * Where the spill should start. It should be passable, otherwise * an empty list gets returned. Consider using * {@link DungeonUtility#getRandomCell(RNG, char[][], Set, int)} * to find it. * @param volume * The number of cells to spill on. * @param impassable the set of chars on the level that block the spill, such * as walls or maybe other spilled things (oil and water). * May be null, which makes this treat '#' as impassable. * @param drunks * The ratio of drunks to use to make the splash more realistic. * Like for dungeon generation, if greater than 0, drunk walkers * will remove the splash's margins, to make it more realistic. * You don't need that if you're doing a splash that is bounded * by walls, because the fill will be realistic. If you're doing * a splash that isn't bounded, use that for its borders not to * be too square. * * <p> * Useful values are 0, 1, and 2. Giving more will likely yield * an empty result, on any decent map sizes. * </p> * @return The spill. It is a list of coordinates (containing {@code start}) * valid in {@code level} that are all adjacent and whose symbol is * passable. If non-empty, this is guaranteed to be an * {@link ArrayList}. */ public static List<Coord> spill(RNG rng, char[][] level, Coord start, int volume, Set<Character> impassable, int drunks) { Set<Character> blocked; if(impassable == null) blocked = new HashSet<>(2); else blocked = impassable; if(splashCache == null || blocked.hashCode() != splashHash) { splashHash = blocked.hashCode(); splashCache = new Splash(blocked); } return splashCache.spill(rng, level, start, volume, drunks); } }