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