// // @(#)Province.java 4/2002 // // Copyright 2002 Zachary DelProposto. All rights reserved. // Use is subject to license terms. // // // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation; either version 2 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with this program; if not, write to the Free Software // Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. // Or from http://www.gnu.org/ // package dip.world; import dip.order.Order; import java.util.*; /** * * A Province represents a region on the map. * <p> * Provinces may be sea, coastal, or landlocked. Their connectivity and type (sea, coastal, * or landlocked) is determined by the Adjacency data. * <p> * The types of provinces are: * <ul> * <li>Landlocked; adjacent only by land (e.g., warsaw) * <li>Sea; adjacent only by water (e.g., black sea) * <li>Single-Coast; (e.g., portugal) * <li>Multi-Coastal; (e.g., spain) * </ul> * <p> * The adjacency information is similar to that used by the Ken Lowe Judge software. * <p> * It is illegal to have a Province without any Coasts, to combine single-coast and * multi-coast, or to have only multiple coasts without a land coast. * * <pre> L S N S W E (land / single / north / south / west / east) =========== x - - - - - landlocked - x - - - - seaspace ("sealocked") x x - - - - coastal land space (only 1 coast) - - ? ? ? ? INVALID - x ? ? ? ? INVALID x x ? ? ? ? INVALID x - ? ? ? ? see below other valid: land + (!single) + (any combination of north/south/west/east) </pre> * <p> * If Supply Centers become more complex in the future, they may * be handled as a separate object within the Province. * */ public class Province implements java.io.Serializable, Comparable { /** * */ private static final long serialVersionUID = 4248077817013191699L; // immutable persistent fields private final String fullName; // fullName MUST BE UNIQUE private final String shortNames[]; // always has AT LEAST one, and all are globally unique private final int index; // contiguous index private final boolean isConvoyableCoast; // 'true' if coast is convoyable private final Adjacency adjacency; // adjacency data // publicly immutable non-final persistent fields // (because of difficulties with creation) private boolean supplyCenter = false; // true if supply center exists here. private Border[] borders = null; // non-zero-length if any Borders exist // transient fields private transient int hashCode = 0; /** * Adjacency maintains the connectivity graph between provinces. */ protected static class Adjacency implements java.io.Serializable { /** * */ private static final long serialVersionUID = -7184227986040255178L; private final HashMap adjLoc; /** * Creates a new Adjacency object. */ private Adjacency() { adjLoc = new HashMap(7); }// Adjacency() /** * Sets which locations are adjacent to the specified coast. * */ protected void setLocations(Coast coast, Location[] locations) { adjLoc.put(coast, locations); }// setLocations() /** * Gets the locations which are adjacent to the coast. * <p> * If no locations are adjacent, a zero-length array is returned. * */ protected Location[] getLocations(Coast coast) { Location[] locations = (Location[]) adjLoc.get(coast); if(locations == null) { locations = Location.EMPTY; } return locations; }// getLocations() /** * Creates a WING coast from Province coastal data. All Coasts must * be set for this Province already. Note that a Wing coast is equiavalent * to 'touching' adjacency. */ protected void createWingCoasts() { HashSet provSet = new HashSet(11); ArrayList locList = new ArrayList(11); for(int i=0; i<Coast.ALL_COASTS.length; i++) { Location[] locs = getLocations(Coast.ALL_COASTS[i]); for(int j=0; j<locs.length; j++) { Province prov = locs[j].getProvince(); if(provSet.add(prov)) { locList.add( new Location(prov, Coast.WING) ); } } } provSet.clear(); setLocations(Coast.WING, (Location[]) locList.toArray(new Location[locList.size()])); }// createWingCoasts() /** * Ensure that adjacency data is consistent, and that there are * no illegal coast combinations. * <p> * The Province argument is required to correctly validate * convoyable coast regions. Convoyable coasts require a single * or multi-coast region with a defined land coast. * <code> * * land / single / north / south / west / east * * x = true; - = false; ? = true or false * * L S N S W E * =========================================== * - - ? ? ? ? INVALID (a) where at least one ? is true * - x ? ? ? ? INVALID (b) where at least one ? is true * x x ? ? ? ? INVALID (c) where at least one ? is true * - - - - - - INVALID (d) * </code> */ protected boolean validate(Province p) { boolean isDirectional = false; for(int i=0; i<Coast.ANY_DIRECTIONAL.length; i++) { if(adjLoc.get(Coast.ANY_DIRECTIONAL[i]) != null) { isDirectional = true; } } final boolean isLand = (adjLoc.get(Coast.LAND) != null); final boolean isSingle = (adjLoc.get(Coast.SINGLE) != null); // covers cases (b) and (c) if(isDirectional && isSingle) { return false; } // covers case (d) if(!isDirectional && !isLand && !isSingle) { return false; } // covers case (a) if(isDirectional && !isLand && !isSingle) { return false; } // check convoyable coasts if(p.isConvoyableCoast() && (!isLand || (!isSingle && !isDirectional)) ) { return false; } return true; }// validate() }// inner class Adjacency() /** * Creates a new Province object. * <b>Unless you are a WorldFactory (or subclass), it should (almost) never be nescessary * to create a Province using new. In fact, to do so will break referential equality. * to get a Province, use the Map.getProvince() and similar methods.</b> * <p> * These are created by a WorldFactory, or through de-serialization. * Null names are not allowed. At least one shortName is required. * */ public Province(String fullName, String[] shortNames, int index, boolean isConvoyableCoast) { if(fullName == null || shortNames == null) { throw new IllegalArgumentException("null full or short name(s)"); } if(shortNames.length < 1) { throw new IllegalArgumentException("at least one shortName required"); } if(index < 0) { throw new IllegalArgumentException("index cannot be negative"); } this.fullName = fullName; this.shortNames = shortNames; this.index = index; this.isConvoyableCoast = isConvoyableCoast; this.adjacency = new Adjacency(); }// Province() /** * Sets the Border data for this province. */ protected void setBorders(Border[] value) { borders = value; }// setBorders() /** * Sets if this province has a supply center. */ protected void setSupplyCenter(boolean value) { supplyCenter = value; }// setSupplyCenter() /** * Returns the Province index; this is an int between 0 (inclusive) and * the total number of provinces (exclusive). It is never negative. */ public final int getIndex() { return index; }// getIndex() /** * Gets the Adjacency data for this Province */ protected final Adjacency getAdjacency() { return adjacency; }// getAdjacency() /** * Get all the Locations that are adjacent to this province. * Note that if you are only interested in adjacent provinces, * getAdjacentLocations(Coast.WING or Coast.TOUCHING) is more appropriate * and faster; however, all Locations returned will be of Coast.WING. * This method will return all truly adjacent locations, which have their * correct coasts. No duplicate Locations will be present in the returned * array. All arrays will be zero-length or higher; a null array is never * returned. */ public Location[] getAllAdjacent() { HashSet locSet = new HashSet(13); ArrayList locList = new ArrayList(13); for(int i=0; i<Coast.ALL_COASTS.length; i++) { Location[] locs = adjacency.getLocations(Coast.ALL_COASTS[i]); for(int j=0; j<locs.length; j++) { Location aLoc = locs[j]; if(locSet.add(aLoc)) { locList.add( aLoc ); } } } return (Location[]) locList.toArray(new Location[locList.size()]); }// getAllAdjacent() /** * Gets the Locations adjacent to this province, given the * specified coast. */ public Location[] getAdjacentLocations(Coast coast) { return adjacency.getLocations(coast); }// getAdjacency() /** Gets the full name (long name) of the Province */ public final String getFullName() { return fullName; } /** * Gets the short name of the Province * <p> This returns the first short name if there are more than one. */ public final String getShortName() { return shortNames[0]; } /** Gets all short names of the Province */ public final String[] getShortNames() { return shortNames; } /** Determine if this Province contains a supply center */ public boolean hasSupplyCenter() { return supplyCenter; } /** * Determines if two provinces are in any way adjacent (connected). * <p> * If two provinces are adjacent, by any coast, this will return true. This * implies connectivity in the broadest sense. No coast information is required * or needed in this or the Province that is compared. <b>because Coasts are * ignored, this method should generally not be used to determine adjacency for the movement * of units.</b> * <p> * This now uses the "Wing" ("Touching") Coast which is equivalent. */ public boolean isTouching(Province province) { /* old code: prior to createWingCoasts() for(int i=0; i<Coast.ALL_COASTS.length; i++) { Location[] locations = adjacency.getLocations(Coast.ALL_COASTS[i]); for(int locIdx=0; locIdx<locations.length; locIdx++) { if(locations[locIdx].getProvince().equals(province)) { return true; } } } */ Location[] locations = adjacency.getLocations(Coast.TOUCHING); for(int locIdx=0; locIdx<locations.length; locIdx++) { if(locations[locIdx].isProvinceEqual(province)) { return true; } } return false; }// isTouching() /** * Checks connectivity between this and another province * <p> * This method only determines if the current Province with the specified * coast is connected to the destination Province. * */ public boolean isAdjacent(Coast sourceCoast, Province dest) { Location[] locations = adjacency.getLocations(sourceCoast); for(int locIdx=0; locIdx<locations.length; locIdx++) { if(locations[locIdx].getProvince().equals(dest)) { return true; } } return false; }// isAdjacent() /** * Checks connectivity between this and another province * <p> * This method only determines if the current Province with the specified * coast is connected to the destination Province and Coast. * <p> * This is a stricter version of isAdjacent(Coast, Province) * */ public boolean isAdjacent(Coast sourceCoast, Location dest) { Location[] locations = adjacency.getLocations(sourceCoast); for(int locIdx=0; locIdx<locations.length; locIdx++) { if(locations[locIdx].equals(dest)) { return true; } } return false; }// isAdjacent() /** Determines if this Province is landlocked. */ public boolean isLandLocked() { for(int i=0; i<Coast.ANY_SEA.length; i++) { if(adjacency.getLocations(Coast.ANY_SEA[i]) != Location.EMPTY) { return false; } } return true; }// isLandLocked() // NOTE: this could be made more efficient /** Determines if this Province is coastal (including multi-coastal). */ public boolean isCoastal() { if(adjacency.getLocations(Coast.LAND) != Location.EMPTY) { for(int i=0; i<Coast.ANY_SEA.length; i++) { Location[] locations = adjacency.getLocations(Coast.ANY_SEA[i]); if(locations.length > 0) { return true; } } } return false; }// isCoastal() /** Determines if this Province is a Land province (landlocked OR coastal) */ public boolean isLand() { return (adjacency.getLocations(Coast.LAND) != Location.EMPTY); }// isLand() /** Determines if this Province is a Sea province (no land, not coastal). */ public boolean isSea() { if(adjacency.getLocations(Coast.LAND) != Location.EMPTY) { return false; } for(int i=0; i<Coast.ANY_DIRECTIONAL.length; i++) { if(adjacency.getLocations(Coast.ANY_DIRECTIONAL[i]) != Location.EMPTY) { return false; } } return true; }// isSea() /** Determines if this Province has multiple coasts (e.g., Spain). */ public boolean isMultiCoastal() { if(adjacency.getLocations(Coast.SEA) == Location.EMPTY) { for(int i=0; i<Coast.ANY_DIRECTIONAL.length; i++) { if(adjacency.getLocations(Coast.ANY_DIRECTIONAL[i]) != Location.EMPTY) { return true; } } } return false; }// isMultiCoastal() /** * Return the coasts supported by this province. * If not multicoastal, returns an empty Coast array. */ public Coast[] getValidDirectionalCoasts() { if(adjacency.getLocations(Coast.SEA) == Location.EMPTY) { ArrayList dir = new ArrayList(4); for(int i=0; i<Coast.ANY_DIRECTIONAL.length; i++) { if(adjacency.getLocations(Coast.ANY_DIRECTIONAL[i]) != Location.EMPTY) { dir.add(Coast.ANY_DIRECTIONAL[i]); } } return (Coast[]) dir.toArray(new Coast[dir.size()]); } return new Coast[0]; }// getValidCoasts() /** Determines if specified coast is allowed for this Province */ public boolean isCoastValid(Coast coast) { if(adjacency.getLocations(coast) == Location.EMPTY) { return false; }; return true; }// isCoastValid() /** Implementation of Object.hashCode() */ public int hashCode() { if(hashCode == 0) { hashCode = fullName.hashCode(); } return hashCode; }// hashCode() public boolean equals(Object o){ if (o instanceof Province){ return this.hashCode() == o.hashCode(); }else{ return false; } } /** Checks if unit can transit from a Location to this Province. */ public boolean canTransit(Location fromLoc, Unit.Type unit, Phase phase, Class orderClass) { return (getTransit(fromLoc, unit, phase, orderClass) == null); }// canTransit() /** Convenient version of canTransit() */ public boolean canTransit(Phase phase, Order order) { return canTransit(order.getSource(), order.getSourceUnitType(), phase, order.getClass()); }// canTransit() /** * Checks if unit can transit from a Location to this Province. Returns the first * failing Border order; returns null if Transit is successfull. */ public Border getTransit(Location fromLoc, Unit.Type unit, Phase phase, Class orderClass) { if(borders != null) { for(int i=0; i<borders.length; i++) { if(!borders[i].canTransit(fromLoc, unit, phase, orderClass)) { return borders[i]; } } } return null; }// getTransit() /** Convenient version of getTransit() */ public Border getTransit(Phase phase, Order order) { return getTransit(order.getSource(), order.getSourceUnitType(), phase, order.getClass()); }// getTransit() /** * Looks through borders to determine if there is a baseMoveModifier. * that fits. Note that the first matching non-zero baseMoveModifier is returned * if there are more than one, which is not recommended. */ public int getBaseMoveModifier(Location fromLoc) { if(borders != null) { for(int i=0; i<borders.length; i++) { final int baseMoveMod = borders[i].getBaseMoveModifier(fromLoc); if(baseMoveMod != 0) { return baseMoveMod; } } } return 0; }// getBaseMoveModifier() /** If this province is a convoyable coastal Province, this will return <code>true</code>. */ public boolean isConvoyableCoast() { return isConvoyableCoast; }// isConvoyableCoast() /** * Indicates if this province is convoyable, either because it is * a Sea province or a convoyable coast. * <p> * No transit checking is performed. */ public boolean isConvoyable() { return (isConvoyableCoast() || isSea()); }// isConvoyable() /* NOTE: we just use default referential equality, since these objects are immutable! */ /** Returns the full name of the province */ public String toString() { return fullName; }// toString(); /** Compares this province to another, by the full name, ignoring case */ public int compareTo(Object obj) { return fullName.compareToIgnoreCase( ((Province) obj).fullName ); }// compareTo() }// class Province