All Downloads are FREE. Search and download functionalities are using the official Maven repository.

squidpony.squidai.WaypointPathfinder Maven / Gradle / Ivy

Go to download

SquidLib platform-independent logic and utility code. Please refer to https://github.com/SquidPony/SquidLib .

There is a newer version: 3.0.6
Show newest version
package squidpony.squidai;

import squidpony.squidgrid.Direction;
import squidpony.squidgrid.Measurement;
import squidpony.squidgrid.Radius;
import squidpony.squidgrid.mapping.DungeonUtility;
import squidpony.squidmath.*;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Map;

import static squidpony.squidmath.CoordPacker.*;

/**
 * Pathfind to known connections between rooms or other "chokepoints" without needing full-map Dijkstra scans.
 * Pre-calculates a path either from or to any given chokepoint to each other chokepoint.
 * Created by Tommy Ettinger on 10/25/2015.
 */
public class WaypointPathfinder {
    private int width;
    private int height;
    private DijkstraMap dm;
    private int[][] expansionMap;
    public StatefulRNG rng;
    private OrderedMap> waypoints;

    /**
     * Calculates and stores the doors and doors-like connections ("chokepoints") on the given map as waypoints.
     * Will use the given Radius enum to determine how to handle DijkstraMap measurement in future pathfinding.
     * Uses a new RNG for all random choices, which will be seeded with {@code rng.nextLong()}, or unseeded if
     * the parameter is null.
     * @param map a char[][] that stores a "complete" dungeon map, with any chars as features that pathfinding needs.
     * @param measurement a Radius that should correspond to how you want path distance calculated.
     * @param rng an RNG object or null (this will always use a new RNG, but it may be seeded by a given RNG's next result)
     */
    public WaypointPathfinder(char[][] map, Radius measurement, IRNG rng)
    {
        if(rng == null)
            this.rng = new StatefulRNG();
        else
            this.rng = new StatefulRNG(rng.nextLong());
        width = map.length;
        height = map[0].length;
        char[][] simplified = DungeonUtility.simplifyDungeon(map);
        OrderedSet centers = PoissonDisk.sampleMap(simplified,
                Math.min(width, height) * 0.4f, this.rng, '#');
        int centerCount = centers.size();
        expansionMap = new int[width][height];
        waypoints = new OrderedMap<>(64);
        dm = new DijkstraMap(simplified, Measurement.MANHATTAN);

        for (Coord center : centers) {
            dm.clearGoals();
            dm.resetMap();
            dm.setGoal(center);
            dm.scan(null, null);
            double current;
            for (int i = 0; i < width; i++) {
                for (int j = 0; j < height; j++) {
                    current = dm.gradientMap[i][j];
                    if (current >= DijkstraMap.FLOOR)
                        continue;
                    if (center.x == i && center.y == j)
                        expansionMap[i][j]++;
                    for (Direction dir : Direction.CARDINALS) {
                        if (dm.gradientMap[i + dir.deltaX][j + dir.deltaY] == current + 1 ||
                                dm.gradientMap[i + dir.deltaX][j + dir.deltaY] == current - 1)
                            expansionMap[i][j]++;
                    }
                }
            }
        }

        for (int i = 0; i < width; i++) {
            for (int j = 0; j < height; j++) {
                expansionMap[i][j] /= centerCount;
            }
        }

        OrderedSet chokes = new OrderedSet<>(128);
        for (int i = 0; i < width; i++) {
            ELEMENT_WISE:
            for (int j = 0; j < height; j++) {
                if(expansionMap[i][j] <= 0)
                    continue;
                int current = expansionMap[i][j];
                boolean good = false;
                for(Direction dir : Direction.CARDINALS) {
                    if (chokes.contains(Coord.get(i + dir.deltaX, j + dir.deltaY)))
                        continue ELEMENT_WISE;
                    if (expansionMap[i + dir.deltaX][j + dir.deltaY] > 0 && expansionMap[i + dir.deltaX][j + dir.deltaY] > current + 1 ||
                            (expansionMap[i + dir.deltaX][j + dir.deltaY] > current && expansionMap[i][j] <= 2)) {
                        if (expansionMap[i - dir.deltaX][j - dir.deltaY] > 0 && expansionMap[i - dir.deltaX][j - dir.deltaY] >= current) {
                            good = true;
                        }
                    }
                }

                if(good) {
                    Coord chk = Coord.get(i, j);
                    chokes.add(chk);
                    waypoints.put(chk, new OrderedMap());
                }
            }
        }
        
        dm = new DijkstraMap(map, DijkstraMap.findMeasurement(measurement));

        for(Map.Entry> n : waypoints.entrySet())
        {
            chokes.remove(n.getKey());
            if(chokes.isEmpty())
                break;
            dm.clearGoals();
            dm.resetMap();
            dm.setGoal(n.getKey());
            dm.scan(null, null);
            for(Coord c : chokes)
            {
                n.getValue().put(c, new Edge(n.getKey(), c, dm.findPathPreScanned(c), dm.gradientMap[c.x][c.y]));
            }
        }

    }
    /**
     * Calculates and stores the doors and doors-like connections ("chokepoints") on the given map as waypoints.
     * Will use the given Radius enum to determine how to handle DijkstraMap measurement in future pathfinding.
     * Uses a new RNG for all random choices, which will be seeded with {@code rng.nextLong()}, or unseeded if
     * the parameter is null.
     * @param map a char[][] that stores a "complete" dungeon map, with any chars as features that pathfinding needs.
     * @param measurement a Radius that should correspond to how you want path distance calculated.
     * @param rng an RNG object or null (this will always use a new RNG, but it may be seeded by a given RNG's next result)
     * @param thickCorridors true if most chokepoints on the map are 2 cells wide instead of 1
     */
    public WaypointPathfinder(char[][] map, Radius measurement, IRNG rng, boolean thickCorridors)
    {
        if(rng == null)
            this.rng = new StatefulRNG();
        else
            this.rng = new StatefulRNG(rng.nextLong());
        width = map.length;
        height = map[0].length;
        char[][] simplified = DungeonUtility.simplifyDungeon(map);
        expansionMap = new int[width][height];
        waypoints = new OrderedMap<>(64);
        OrderedSet chokes = new OrderedSet<>(128);

        if(thickCorridors)
        {
            short[] floors = pack(simplified, '.'),
                    rooms = flood(floors, retract(floors, 1, 60, 60, true), 2, false),
                    corridors = differencePacked(floors, rooms),
                    doors = intersectPacked(rooms, fringe(corridors, 1, 60, 60, false));
            Coord[] apart = apartPacked(doors, 1);
            Collections.addAll(chokes, apart);
            for (int i = 0; i < apart.length; i++) {
                waypoints.put(apart[i], new OrderedMap());
            }
        }
        else {
            OrderedSet centers = PoissonDisk.sampleMap(simplified,
                    Math.min(width, height) * 0.4f, this.rng, '#');
            int centerCount = centers.size();
            dm = new DijkstraMap(simplified, Measurement.MANHATTAN);

            for (Coord center : centers) {
                dm.clearGoals();
                dm.resetMap();
                dm.setGoal(center);
                dm.scan(null, null);
                double current;
                for (int i = 0; i < width; i++) {
                    for (int j = 0; j < height; j++) {
                        current = dm.gradientMap[i][j];
                        if (current >= DijkstraMap.FLOOR)
                            continue;
                        if (center.x == i && center.y == j)
                            expansionMap[i][j]++;
                        for (Direction dir : Direction.CARDINALS) {
                            if (dm.gradientMap[i + dir.deltaX][j + dir.deltaY] == current + 1 ||
                                    dm.gradientMap[i + dir.deltaX][j + dir.deltaY] == current - 1)
                                expansionMap[i][j]++;
                        }
                    }
                }
            }

            for (int i = 0; i < width; i++) {
                for (int j = 0; j < height; j++) {
                    expansionMap[i][j] /= centerCount;
                }
            }

            for (int i = 0; i < width; i++) {
                ELEMENT_WISE:
                for (int j = 0; j < height; j++) {
                    if (expansionMap[i][j] <= 0)
                        continue;
                    int current = expansionMap[i][j];
                    boolean good = false;
                    for (Direction dir : Direction.CARDINALS) {
                        if (chokes.contains(Coord.get(i + dir.deltaX, j + dir.deltaY)))
                            continue ELEMENT_WISE;
                        if (expansionMap[i + dir.deltaX][j + dir.deltaY] > 0 && expansionMap[i + dir.deltaX][j + dir.deltaY] > current + 1 ||
                                (expansionMap[i + dir.deltaX][j + dir.deltaY] > current && expansionMap[i][j] <= 2)) {
                            if (expansionMap[i - dir.deltaX][j - dir.deltaY] > 0 && expansionMap[i - dir.deltaX][j - dir.deltaY] >= current) {
                                good = true;
                            }
                        }
                    }

                    if (good) {
                        Coord chk = Coord.get(i, j);
                        chokes.add(chk);
                        waypoints.put(chk, new OrderedMap());
                    }
                }
            }
        }

        dm = new DijkstraMap(map, DijkstraMap.findMeasurement(measurement));

        int e = 0;
        for(Map.Entry> n : waypoints.entrySet())
        {
            chokes.remove(n.getKey());
            if(chokes.isEmpty())
                break;
            dm.clearGoals();
            dm.resetMap();
            dm.setGoal(n.getKey());
            dm.scan(null, null);
            for(Coord c : chokes)
            {
                n.getValue().put(c, new Edge(n.getKey(), c, dm.findPathPreScanned(c), dm.gradientMap[c.x][c.y]));
            }
        }

    }

    /**
     * Calculates and stores the specified fraction of walkable points from map as waypoints. Does not perform any
     * analysis of chokepoints and acts as a more brute-force solution when maps may be unpredictable. The lack of an
     * analysis step may mean this could have drastically less of a penalty to startup time than the other constructors,
     * and with the right fraction parameter (29 seems ideal), may perform better as well. Will use the given Radius
     * enum to determine how to handle DijkstraMap measurement in future pathfinding.
     * Uses a new RNG for all random choices, which will be seeded with {@code rng.nextLong()}, or unseeded if
     * the parameter is null.
     * 
* Remember, a fraction value of 29 works well! * @param map a char[][] that stores a "complete" dungeon map, with any chars as features that pathfinding needs. * @param measurement a Radius that should correspond to how you want path distance calculated. * @param rng an RNG object or null (this will always use a new RNG, but it may be seeded by a given RNG's next result) * @param fraction the fractional denominator of passable cells to assign as waypoints; use 29 if you aren't sure */ public WaypointPathfinder(char[][] map, Radius measurement, IRNG rng, int fraction) { if(rng == null) this.rng = new StatefulRNG(); else this.rng = new StatefulRNG(rng.nextLong()); width = map.length; height = map[0].length; char[][] simplified = DungeonUtility.simplifyDungeon(map); expansionMap = new int[width][height]; waypoints = new OrderedMap<>(64); OrderedSet chokes = new OrderedSet<>(128); short[] floors = pack(simplified, '.'); Coord[] apart = fractionPacked(floors, fraction); Collections.addAll(chokes, apart); for (int i = 0; i < apart.length; i++) { waypoints.put(apart[i], new OrderedMap()); } dm = new DijkstraMap(map, DijkstraMap.findMeasurement(measurement)); int e = 0; for(Map.Entry> n : waypoints.entrySet()) { chokes.remove(n.getKey()); if(chokes.isEmpty()) break; dm.clearGoals(); dm.resetMap(); dm.setGoal(n.getKey()); dm.scan(null, null); for(Coord c : chokes) { n.getValue().put(c, new Edge(n.getKey(), c, dm.findPathPreScanned(c), dm.gradientMap[c.x][c.y])); } } } /** * Calculates and stores the doors and doors-like connections ("chokepoints") on the given map as waypoints. * Will use the given DijkstraMap for pathfinding after construction (and during some initial calculations). * The dijkstra parameter will be mutated by this class, so it should not be reused elsewhere. * Uses a new RNG for all random choices, which will be seeded with {@code rng.nextLong()}, or unseeded if * the parameter is null. * @param map a char[][] that stores a "complete" dungeon map, with any chars as features that pathfinding needs * @param dijkstra a DijkstraMap that will be used to find paths; may have costs but they will not be used * @param rng an RNG object or null (this will always use a new RNG, but it may be seeded by a given RNG's next result) */ public WaypointPathfinder(char[][] map, DijkstraMap dijkstra, IRNG rng) { if(rng == null) this.rng = new StatefulRNG(); else this.rng = new StatefulRNG(rng.nextLong()); width = map.length; height = map[0].length; char[][] simplified = DungeonUtility.simplifyDungeon(map); OrderedSet centers = PoissonDisk.sampleMap(simplified, Math.min(width, height) * 0.4f, this.rng, '#'); int centerCount = centers.size(); expansionMap = new int[width][height]; waypoints = new OrderedMap<>(64); dm = new DijkstraMap(simplified, Measurement.MANHATTAN); for (Coord center : centers) { dm.clearGoals(); dm.resetMap(); dm.setGoal(center); dm.scan(null, null); double current; for (int i = 0; i < width; i++) { for (int j = 0; j < height; j++) { current = dm.gradientMap[i][j]; if (current >= DijkstraMap.FLOOR) continue; if (center.x == i && center.y == j) expansionMap[i][j]++; for (Direction dir : Direction.CARDINALS) { if (dm.gradientMap[i + dir.deltaX][j + dir.deltaY] == current + 1 || dm.gradientMap[i + dir.deltaX][j + dir.deltaY] == current - 1) expansionMap[i][j]++; } } } } for (int i = 0; i < width; i++) { for (int j = 0; j < height; j++) { expansionMap[i][j] /= centerCount; } } OrderedSet chokes = new OrderedSet<>(128); for (int i = 0; i < width; i++) { ELEMENT_WISE: for (int j = 0; j < height; j++) { if(expansionMap[i][j] <= 0) continue; int current = expansionMap[i][j]; boolean good = false; for(Direction dir : Direction.CARDINALS) { if (chokes.contains(Coord.get(i + dir.deltaX, j + dir.deltaY))) continue ELEMENT_WISE; if (expansionMap[i + dir.deltaX][j + dir.deltaY] > 0 && expansionMap[i + dir.deltaX][j + dir.deltaY] > current + 1 || (expansionMap[i + dir.deltaX][j + dir.deltaY] > current && expansionMap[i][j] <= 2)) { if (expansionMap[i - dir.deltaX][j - dir.deltaY] > 0 && expansionMap[i - dir.deltaX][j - dir.deltaY] >= current) { good = true; } } } if(good) { Coord chk = Coord.get(i, j); chokes.add(chk); waypoints.put(chk, new OrderedMap()); } } } dm = dijkstra; int e = 0; for(Map.Entry> n : waypoints.entrySet()) { chokes.remove(n.getKey()); if(chokes.isEmpty()) break; dm.clearGoals(); dm.resetMap(); dm.setGoal(n.getKey()); dm.scan(null, null); for(Coord c : chokes) { n.getValue().put(c, new Edge(n.getKey(), c, dm.findPathPreScanned(c), dm.gradientMap[c.x][c.y])); } } } /** * Finds the appropriate one of the already-calculated, possibly-long paths this class stores to get from a waypoint * to another waypoint, then quickly finds a path to get on the long path, and returns the total path. This does * not need to perform any full-map scans with DijkstraMap. * @param self the pathfinder's position * @param approximateTarget the Coord that represents the approximate area to pathfind to; will be randomized if * it is not walkable. * @return an ArrayList of Coord that will go from a cell adjacent to self to a waypoint near approximateTarget */ public ArrayList getKnownPath(Coord self, Coord approximateTarget) { ArrayList near = dm.findNearestMultiple(approximateTarget, 5, waypoints.keySet()); Coord me = dm.findNearest(self, waypoints.keySet()); double bestCost = 999999.0; ArrayList path = new ArrayList<>(); /*if (waypoints.containsKey(me)) { Edge[] ed = waypoints.get(me).values().toArray(new Edge[waypoints.get(me).size()]); Arrays.sort(ed); path = ed[0].path; */ boolean reversed = false; for (Coord test : near) { if (waypoints.containsKey(test)) { Edge ed; if(waypoints.get(test).containsKey(me)) { ed = waypoints.get(test).get(me); reversed = true; } else if(waypoints.containsKey(me) && waypoints.get(me).containsKey(test)) ed = waypoints.get(me).get(test); else continue; if (ed.cost < bestCost) { bestCost = ed.cost; path = new ArrayList<>(ed.path); } } } if(path.isEmpty()) return path; if(reversed) Collections.reverse(path); ArrayList getToPath = dm.findShortcutPath(self, path.toArray(new Coord[0])); if (getToPath.size() > 0) { getToPath.remove(getToPath.size() - 1); getToPath.addAll(path); path = getToPath; } return path; } /** * If a creature is interrupted or obstructed on a "highway" path, it may need to travel off the path to its goal. * This method gets a straight-line path back to the path to goal. It does not contain the "highway" path, only the * "on-ramp" to enter the ideal path. * @param currentPosition the current position of the pathfinder, which is probably not on the ideal path * @param path the ideal path, probably returned by getKnownPath * @return an ArrayList of Coord that go from a cell adjacent to currentPosition to a Coord on or adjacent to path. */ public ArrayList goBackToPath(Coord currentPosition, ArrayList path) { return dm.findShortcutPath(currentPosition, path.toArray(new Coord[0])); } public OrderedSet getWaypoints() { return waypoints.keysAsOrderedSet(); } private static class Edge implements Comparable { public Coord from; public Coord to; public ArrayList path; public double cost; public Edge(Coord from, Coord to, ArrayList path, double cost) { this.from = from; this.to = to; this.path = path; this.cost = cost; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Edge edge = (Edge) o; if (Double.compare(edge.cost, cost) != 0) return false; if (!from.equals(edge.from)) return false; return to.equals(edge.to); } @Override public int hashCode() { int result; long temp; result = from.hashCode(); result = 31 * result + to.hashCode(); temp = NumberTools.doubleToLongBits(cost); result = 31 * result + (int) (temp ^ (temp >>> 32)); return result; } /** * Compares this object with the specified object for order. Returns a * negative integer, zero, or a positive integer as this object is less * than, equal to, or greater than the specified object. * * Note: this class has a natural ordering that is * inconsistent with equals. * @param o the object to be compared. * @return a negative integer, zero, or a positive integer as this object * is less than, equal to, or greater than the specified object. * @throws NullPointerException if the specified object is null * @throws ClassCastException if the specified object's type prevents it * from being compared to this object. */ @Override public int compareTo(Edge o) { return (cost - o.cost > 0) ? 1 : (cost - o.cost < 0) ? -1 : 0; } } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy