001package squidpony.squidai; 002 003import squidpony.squidgrid.Direction; 004import squidpony.squidgrid.Radius; 005import squidpony.squidmath.*; 006 007import java.io.Serializable; 008import java.util.Collection; 009 010/** 011 * Calculates the Zone of Influence, also known as Zone of Control, for different points on a map. 012 * Uses GreasedRegion for faster storage and manipulation of zones; it's suggested if you use this class to be 013 * somewhat familiar with the methods for manipulating data in that class, though a GreasedRegion can also be used just 014 * like a Collection of Coord values. This class is very similar in API and implementation to {@link ZOI}, but should be 015 * faster on large maps and produces much less garbage. The main reason to choose between ZOI and 016 * GreasedZOI is whether your existing code uses GreasedRegions, like this class, or uses CoordPacker, like ZOI. If you 017 * don't currently use either, GreasedZOI is probably preferable because the {@link #calculate()} method produces a 018 * value that can be reasonably consumed by Collection-based APIs, while {@link ZOI#calculate()} produces a 019 * harder-to-use {@code short[]} that must be read by CoordPacker. Repeated operations on CoordPacker's {@code short[]} 020 * data will have to keep allocating more arrays and temporary internal data structures, while a GreasedRegion can be 021 * reused many times and only rarely allocates more space. 022 * <br> 023 * Created by Tommy Ettinger on 1/14/2018. 024 */ 025public class GreasedZOI implements Serializable { 026 private static final long serialVersionUID = 2L; 027 private DijkstraMap dijkstra; 028 private Coord[][] influences; 029 private GreasedRegion[] groups; 030 private boolean completed; 031 private Radius radius; 032 /** 033 * Constructs a Zone of Influence map. Takes a (quite possibly jagged) array of arrays of Coord influences, where 034 * the elements of the outer array represent different groups of influencing "factions" or groups that exert control 035 * over nearby areas, and the Coord elements of the inner array represent individual spots that are part of those 036 * groups and share influence with all Coord in the same inner array. Also takes a char[][] for a map, which can be 037 * the simplified map with only '#' for walls and '.' for floors, or the final map (with chars like '~' for deep 038 * water as well as walls and floors), and a Radius enum that will be used to determine how distance is calculated. 039 * <br> 040 * Call {@link #calculate()} when you want information out of this. 041 * @param influences an outer array containing influencing groups, each an array containing Coords that influence 042 * @param map a char[][] that is used as an area map; should be bounded 043 * @param radiusStrategy a Radius enum that corresponds to how distance should be measured 044 */ 045 public GreasedZOI(Coord[][] influences, char[][] map, Radius radiusStrategy) { 046 this.influences = influences; 047 groups = new GreasedRegion[influences.length]; 048 radius = radiusStrategy == null ? Radius.CIRCLE : radiusStrategy; 049 dijkstra = new DijkstraMap(map, radius.matchingMeasurement()); 050 } 051 /** 052 * Constructs a Zone of Influence map. Takes an arrays of Coord influences, where each Coord is treated as both a 053 * one-element group of influencing "factions" or groups that exert control over nearby areas, and the individual 054 * spot that makes up one of those groups and spreads influence. Also takes a char[][] for a map, which can be the 055 * simplified map with only '#' for walls and '.' for floors, or the final map (with chars like '~' for deep water 056 * as well as walls and floors), and a Radius enum that will be used to determine how distance is calculated. 057 * <br> 058 * Essentially, this is the same as constructing a ZOI with a Coord[][] where each inner array has only one element. 059 * <br> 060 * Call {@link #calculate()} when you want information out of this. 061 * @param influences an array containing Coords that each have their own independent influence 062 * @param map a char[][] that is used as an area map; should be bounded 063 * @param radiusStrategy a Radius enum that corresponds to how distance should be measured 064 * @see squidpony.squidmath.PoissonDisk PoissonDisk provides a good way to generate evenly spaced Coords 065 */ 066 public GreasedZOI(Coord[] influences, char[][] map, Radius radiusStrategy) { 067 this.influences = new Coord[influences.length][]; 068 for (int i = 0; i < influences.length; i++) { 069 this.influences[i] = new Coord[] { influences[i] }; 070 } 071 groups = new GreasedRegion[influences.length]; 072 radius = radiusStrategy == null ? Radius.CIRCLE : radiusStrategy; 073 dijkstra = new DijkstraMap(map, radius.matchingMeasurement()); 074 } 075 /** 076 * Constructs a Zone of Influence map. Takes a Collection of Coord influences, where each Coord is treated as both a 077 * one-element group of influencing "factions" or groups that exert control over nearby areas, and the individual 078 * spot that makes up one of those groups and spreads influence. Also takes a char[][] for a map, which can be the 079 * simplified map with only '#' for walls and '.' for floors, or the final map (with chars like '~' for deep water 080 * as well as walls and floors), and a Radius enum that will be used to determine how distance is calculated. 081 * <br> 082 * Essentially, this is the same as constructing a ZOI with a Coord[][] where each inner array has only one element, 083 * drawn from a Collection. It often makes sense to use a GreasedRegion as the Collection of Coord. 084 * <br> 085 * Call {@link #calculate()} when you want information out of this. 086 * @param influences A Collection of Coord, such as a GreasedRegion, where each Coord has independent influence 087 * @param map a char[][] that is used as an area map; should be bounded 088 * @param radiusStrategy a Radius enum that corresponds to how distance should be measured 089 */ 090 public GreasedZOI(Collection<Coord> influences, char[][] map, Radius radiusStrategy) { 091 this.influences = new Coord[influences.size()][]; 092 int i = 0; 093 for(Coord c : influences) { 094 this.influences[i++] = new Coord[]{c}; 095 } 096 groups = new GreasedRegion[this.influences.length]; 097 radius = radiusStrategy == null ? Radius.CIRCLE : radiusStrategy; 098 dijkstra = new DijkstraMap(map, radius.matchingMeasurement()); 099 } 100 101 /** 102 * Finds the zones of influence for each of the influences (inner arrays of Coord) this was constructed with, and 103 * returns all zones as a GreasedRegion array. This has each zone of influence overlap with its neighbors; this 104 * is useful to find borders using {@link GreasedRegion#and(GreasedRegion)}, and borders are typically between 1 and 105 * 2 cells wide. You can get a different region if you want region A without the overlapping areas it shares with 106 * region B by using {@link GreasedRegion#andNot(GreasedRegion)}. Merging two zones A and B can be done with 107 * {@link GreasedRegion#or(GreasedRegion)}. You can transform the data into a boolean[][] easily with 108 * {@link GreasedRegion#decode()}, where true is contained in the zone and false is not. The methods 109 * {@link GreasedRegion#fringe()}, {@link GreasedRegion#expand()}, {@link GreasedRegion#singleRandom(IRNG)}, and 110 * {@link GreasedRegion#separatedBlue(double)} are also potentially useful for this sort of data. You should save 111 * the {@code GreasedRegion[]} for later use if you want to call 112 * {@link #nearestInfluences(GreasedRegion[], Coord)}. 113 * <br> 114 * The first GreasedRegion in the returned GreasedRegion[] will correspond to the area influenced by the first 115 * Coord[] in the nested array passed to the constructor (or the first Coord if a non-nested array was passed); the 116 * second will correspond to the second, and so on. The length of the GreasedRegion[] this returns will equal the 117 * number of influence groups. 118 * @return a GreasedRegion array, with each item storing a zone's area 119 */ 120 public GreasedRegion[] calculate() 121 { 122 for (int i = 0; i < influences.length; i++) { 123 for (int j = 0; j < influences[i].length; j++) { 124 dijkstra.setGoal(influences[i][j]); 125 } 126 } 127 dijkstra.scan(null, null); 128 final double[][] scannedAll = dijkstra.gradientMap; 129 130 for (int i = 0; i < influences.length; i++) { 131 132 /* 133 dijkstra.clearGoals(); 134 dijkstra.resetMap(); 135 for (int j = 0; j < influences[i].length; j++) { 136 dijkstra.setGoal(influences[i][j]); 137 } 138 double[][] factionScanned = dijkstra.scan(null); 139 for (int y = 0; y < map[0].length; y++) { 140 for (int x = 0; x < map.length; x++) { 141 influenced[x][y] = (scannedAll[x][y] < DijkstraMap.FLOOR) && 142 (factionScanned[x][y] - scannedAll[x][y] <= 1); 143 } 144 }*/ 145 groups[i] = increasing(scannedAll, influences[i]); 146 } 147 completed = true; 148 return groups; 149 } 150 protected GreasedRegion increasing(double[][] dm, Coord[] inf) { 151 OrderedSet<Coord> open = new OrderedSet<>(inf), fresh = new OrderedSet<>(64); 152 Direction[] dirs = (radius.equals2D(Radius.DIAMOND)) ? Direction.CARDINALS : Direction.OUTWARDS; 153 GreasedRegion influenced = new GreasedRegion(dijkstra.width, dijkstra.height); 154 final int width = dm.length; 155 final int height = width == 0 ? 0 : dm[0].length; 156 157 int numAssigned = open.size(); 158 double diff; 159 while (numAssigned > 0) { 160 numAssigned = 0; 161 for (Coord cell : open) { 162 influenced.insert(cell); 163 for (int d = 0; d < dirs.length; d++) { 164 Coord adj = cell.translate(dirs[d].deltaX, dirs[d].deltaY); 165 if (adj.x < 0 || adj.y < 0 || width <= adj.x || height <= adj.y) 166 /* Outside the map */ 167 continue; 168 if (!open.contains(adj) && dm[adj.x][adj.y] < DijkstraMap.FLOOR && !influenced.contains(adj)) { 169 //h = heuristic(dirs[d]); 170 diff = dm[adj.x][adj.y] - dm[cell.x][cell.y]; 171 if (diff <= 1.0 && diff >= 0) { 172 fresh.add(adj); 173 influenced.insert(adj); 174 ++numAssigned; 175 } 176 } 177 } 178 } 179 180 open = new OrderedSet<>(fresh); 181 fresh.clear(); 182 } 183 184 return influenced; 185 } 186 187 /** 188 * Given the zones resulting from this class' {@link #calculate()} method and a Coord to check, finds the indices of 189 * all influencing groups in zones that have the Coord in their area, and returns all such indices as an IntVLA. 190 * @param zones a GreasedRegion[] returned by calculate 191 * @param point the Coord to test 192 * @return an IntVLA where each element is the index of an influencing group in zones 193 */ 194 public IntVLA nearestInfluences(GreasedRegion[] zones, Coord point) 195 { 196 IntVLA found = new IntVLA(4); 197 for (int i = 0; i < zones.length; i++) { 198 if(zones[i].contains(point)) 199 found.add(i); 200 } 201 return found; 202 } 203 /** 204 * This can be given a Coord to check in the results of the latest calculate() call. Finds the indices of all 205 * influencing groups in zones that have the Coord in their area, and returns all such indices as an {@link IntVLA}. 206 * You can convert the IntVLA to an array with {@link IntVLA#toArray()} if you want to match ZOI's behavior. 207 * 208 * @param point the Coord to test 209 * @return an IntVLA where each element is the index of an influencing group in zones 210 */ 211 public IntVLA nearestInfluences(Coord point) 212 { 213 if(!completed) 214 return new IntVLA(0); 215 IntVLA found = new IntVLA(4); 216 for (short i = 0; i < groups.length; i++) { 217 if(groups[i].contains(point)) 218 found.add(i); 219 } 220 return found; 221 } 222 223 /** 224 * Gets the influencing groups; ideally the result should not be changed without setting it back with setInfluences. 225 * @return influences a jagged array of Coord arrays, where the inner arrays are groups of influences 226 */ 227 public Coord[][] getInfluences() { 228 return influences; 229 } 230 231 /** 232 * Changes the influencing groups. This also invalidates the last calculation for the purposes of nearestInfluences, 233 * at least for the overload that takes only a Coord. 234 * @param influences a jagged array of Coord arrays, where the inner arrays are groups of influences 235 */ 236 public void setInfluences(Coord[][] influences) { 237 if(groups.length == influences.length) 238 { 239 for (int i = 0; i < groups.length; i++) { 240 groups[i].clear(); 241 } 242 } 243 else 244 groups = new GreasedRegion[influences.length]; 245 this.influences = influences; 246 completed = false; 247 } 248}