// // @(#)Border.java 10/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 dip.misc.Utils; import java.io.Serializable; import java.lang.reflect.Field; import java.util.ArrayList; import java.util.StringTokenizer; import java.util.Arrays; /** * * A Border limits movement or support between 2 provinces. * * A Border object is immutable. * * The DTD for a Border object is:<br> * <code> * <!ATTLIST BORDER <br> * id ID #REQUIRED<br> * description CDATA #REQUIRED<br> * from CDATA #IMPLIED<br> * unitTypes CDATA #IMPLIED<br> * orderTypes CDATA #IMPLIED<br> * year CDATA #IMPLIED<br> * season CDATA #IMPLIED<br> * phase CDATA #IMPLIED<br> * baseMoveModifier CDATA #IMPLIED<br> * > * </code> * <p> * Therefore, all fields are optional except for "id" and "description". * If a field is not specified, it is assumed to apply to all types. * Therefore: a border with a unitType of "Army" and year of "1900, 2000" would * prohibit Armies from passing during the 1900 to 2000 years. However, if the * unitType was omitted, no unit could pass (Army, Wing, or Fleet) from 1900 to * 2000 years. * <p> * The exception to this is the "from" field. If a "from" field is present, * the other criteria apply ONLY if "from" matches. * <p> * All specified items (except baseMoveModifier and from), thus unitTypes/ * orderTypes/year/season/phase) must match for the Border to prohibit * crossing. * <p> * Borders apply to ANY crossing; this includes Movement as well as Support. * <p> * <b>Field Values:</b> * <ul> * <li><b>id: </b>The unique ID that identified this Border. These * IDs are used in subsequent PROVINCE definitions. Therefore, * a Border can be used multiple times.</li> * <li><b>description: </b>If the border prohibits movement, this * text message is displayed.</li> * <li><b>from: </b>The locations from which units are coming that this * applies to. Optional. See above.</li> * <li><b>unitTypes: </b> The unit types to which this applies. * Optional. </li> * <li><b>orderTypes: </b> The order types (e.g., dip.order.Move) to which * this applies. Optional. </li> * <li><b>year: </b> The years for which this applies. TWO year values * must be specified; the first is the minimum, the second is the maximum. * Alternatively, the phrase "odd" (for odd years) or "even" (for even * years) may be used. Both minimum and maximum are inclusive. * Thus to specify a single year: "2000, 2000"; a range: "1900, 2000"; * even years: "even". Optional. </li> * <li><b>season: </b> The seasons (e.g., "Fall", "Spring") to * which this applies. Optional. </li> * <li><b>phase: </b>The phases (e.g., "Movement", "Retreat") to * to which this applies. Note that the Adjustment phase is not * allowed, as adjustments (adding/removing units) do not ocucr * via borders. Optional. </li> * <li><b>baseMoveModifier: </b> An optional modifier of Move strength. * This can be positive or negative. If not specified, it is assumed * to be 0. If <b>from</b> is specified, this will only apply to * the specified locations. Optional. </li> * </ul> */ public class Border implements Serializable { /** Constant indicating year was omitted */ private static final int YEAR_NOT_SPECIFIED = 0; /** Constant indicating year is ranged */ private static final int YEAR_SPECIFIED = 1; /** Constant indicating that the transit allowed only during odd years */ private static final int YEAR_ODD = 2; /** Constant indicating that the transit allowed only during even years */ private static final int YEAR_EVEN = 3; private static final String TOK_YEAR_ODD = "odd"; private static final String TOK_YEAR_EVEN = "even"; // instance fields private final Location[] from; // location(s) from which this transit limit applies; // if null, applies to all 'from' locations. // may specify coasts; if coast not defined, any coast used private final Phase.SeasonType[] seasons; // if null, applies to all seasons private final Phase.PhaseType[] phases; // if null, applies to all phases private final Unit.Type[] unitTypes; // if null, applies to all unit types private final String description; // description private final Class[] orderClasses; // if null, applies to all order types private int yearMin = 0; private int yearMax = 0; private int yearModifier = YEAR_NOT_SPECIFIED; // if not specified, this is the result // not determinants in canTransit() private final int baseMoveModifier; // support modifier (defaults to 0) private final String id; // identifying name /** * Constructor. The String arguments are parsed; if they are not valid, * an InvalidBorderException will be thrown. It is not recommended that * null arguments are given. Instead, use empty strings or public constants * where appropriate. * <p> * The from Locations may be null, if that field is empty. * * @throws InvalidBorderException if any arguments are invalid. * @throws IllegalArgumentException if id, description, or prohibited is null */ public Border(String id, String description, String units, Location[] from, String orders, String baseMoveModifier, String season, String phase, String year) throws InvalidBorderException { if(id == null || description == null || units == null || orders == null || season == null || phase == null || year == null) { throw new IllegalArgumentException(); } // set id. This is used by error messages, so must be set early. this.id = id; // parse allowed orderClasses via order classes; must specify package [case sensitive] // e.g.: dip.order.Move // these may be separated by spaces or commas (or both) orderClasses = parseOrders(orders); // parse unitTypes; must specify package [case sensitive] // e.g.: ARMY; must be a declared unit constant in dip.world.Unit unitTypes = parseUnitTypes(units); this.seasons = parseProhibitedSeasons(season); this.phases = parseProhibitedPhases(phase); parseYear(year); this.baseMoveModifier = parseBaseMoveModifier(baseMoveModifier); // fields we don't need to parse this.from = from; this.description = description; /* System.out.println("BORDER created:"); System.out.println(" ID: "+id); System.out.println(" from: "+toList(from)); System.out.println(" seasons: "+toList(seasons)); System.out.println(" phases: "+toList(phases)); System.out.println(" unitTypes: "+toList(unitTypes)); System.out.println(" orderClasses: "+toList(orderClasses)); System.out.println(" yearMin: "+yearMin); System.out.println(" yearMax: "+yearMax); System.out.println(" yearModifier: "+yearModifier); System.out.println(" bmm: "+baseMoveModifier); */ }// Border() // TEMP private static String toList(Object[] obj) { if(obj != null) { return Arrays.asList(obj).toString(); } return "null"; } /** Parses the prohibited SeasonTypes (uses Phase.SeasonTypes.parse()) */ private Phase.SeasonType[] parseProhibitedSeasons(String in) throws InvalidBorderException { StringTokenizer st = new StringTokenizer(in, ", "); ArrayList list = new ArrayList(); while(st.hasMoreTokens()) { String tok = st.nextToken().trim(); Phase.SeasonType season = Phase.SeasonType.parse(tok); if(season == null) { throw new InvalidBorderException("Border "+id+": season \""+tok+"\" is not recognized."); } list.add(season); } if(list.isEmpty()) { return null; } else { return (Phase.SeasonType[]) list.toArray(new Phase.SeasonType[list.size()]); } }// parseProhibitedSeasons() /** Parses the prohibited PhaseTypes (uses Phase.PhaseType.parse()) */ private Phase.PhaseType[] parseProhibitedPhases(String in) throws InvalidBorderException { StringTokenizer st = new StringTokenizer(in, ", "); ArrayList list = new ArrayList(); while(st.hasMoreTokens()) { String tok = st.nextToken().trim(); Phase.PhaseType phase = Phase.PhaseType.parse(tok); if(phase == null || Phase.PhaseType.ADJUSTMENT.equals(phase)) { throw new InvalidBorderException("Border "+id+": phase \""+tok+"\" is not allowed or recognized."); } list.add(phase); } if(list.isEmpty()) { return null; } else { return (Phase.PhaseType[]) list.toArray(new Phase.PhaseType[list.size()]); } }// parseProhibitedPhases() /** * Parses the year value (integer) * Expecting: * ####, #### (min/max) * odd * even */ private void parseYear(final String in) throws InvalidBorderException { if(in == null) { throw new IllegalArgumentException(); } yearMin = Integer.MIN_VALUE; yearMax = Integer.MAX_VALUE; yearModifier = YEAR_SPECIFIED; // empty case final String text = in.trim(); if("".equals(text)) { yearModifier = YEAR_NOT_SPECIFIED; } else { StringTokenizer st = new StringTokenizer(in, ", \t"); String value1 = null; String value2 = null; if(st.hasMoreTokens()) { value1 = st.nextToken(); } if(st.hasMoreTokens()) { value2 = st.nextToken(); } if(st.hasMoreTokens() || value1 == null) { throw new InvalidBorderException( Utils.getLocalString("Border.error.badyear", id, "Too few / too many year tokens.")); } if(TOK_YEAR_ODD.equalsIgnoreCase(value1)) { yearModifier = YEAR_ODD; if(value2 != null) { throw new InvalidBorderException( Utils.getLocalString("Border.error.badyear", id, "Cannot specify even/odd + year")); } } else if(TOK_YEAR_EVEN.equalsIgnoreCase(value1)) { yearModifier = YEAR_EVEN; if(value2 != null) { throw new InvalidBorderException( Utils.getLocalString("Border.error.badyear", id, "Cannot specify even/odd + year")); } } else { try { yearMin = Integer.parseInt(value1); yearMax = Integer.parseInt(value2); if(yearMin > yearMax) { throw new NumberFormatException(); } } catch(NumberFormatException e) { throw new InvalidBorderException( Utils.getLocalString("Border.error.badyear", id, "Minimum and Maximum year values not specified or illegal.")); } } } return; }// parseYear() /** Parses the unit types */ private Unit.Type[] parseUnitTypes(String in) throws InvalidBorderException { ArrayList list = new ArrayList(10); StringTokenizer st = new StringTokenizer(in,", "); while(st.hasMoreTokens()) { String tok = st.nextToken(); final Unit.Type ut = Unit.Type.parse(tok); if(ut == null) { throw new InvalidBorderException(Utils.getLocalString("Border.error.badunit", id, tok)); } list.add( ut ); } if(list.isEmpty()) { return null; } else { return (Unit.Type[]) list.toArray(new Unit.Type[list.size()]); } }// parseUnitTypes() /** Parses the order types */ private Class[] parseOrders(String in) throws InvalidBorderException { final Class[] classes = parseClasses2Objs(in, "dip.order.Order"); if(classes.length == 0) { return null; } return classes; }// parseOrders() /** Internal parser helper method */ private Class[] parseClasses2Objs(String in, String superClassName) throws InvalidBorderException { Class superClass = null; try { superClass = Class.forName(superClassName); } catch(ClassNotFoundException e) { throw new InvalidBorderException(Utils.getLocalString("Border.error.internal", "parseClasses2Objs()", e.getMessage())); } ArrayList list = new ArrayList(10); StringTokenizer st = new StringTokenizer(in,", "); while(st.hasMoreTokens()) { String tok = st.nextToken(); Class cls = null; try { cls = Class.forName(tok); } catch(ClassNotFoundException cnfe) { throw new InvalidBorderException(Utils.getLocalString("Border.error.badclass", id, tok)); } if( !superClass.isAssignableFrom(cls) ) { throw new InvalidBorderException(Utils.getLocalString("Border.error.badderivation", id, cls.getName(), superClass.getName())); } list.add(cls); } return (Class[]) list.toArray(new Class[list.size()]); }// parseClasses2Objs() /** * Parses the base move modifier. If string is empty, defaults to 0. * The format is just a positive or negative (or 0) integer. */ private int parseBaseMoveModifier(String in) throws InvalidBorderException { in = in.trim(); if(in.length() == 0) { return 0; } try { return Integer.parseInt(in); } catch(NumberFormatException e) { // fall through to exception, below } throw new InvalidBorderException(Utils.getLocalString("Border.error.badmovemod", id, in)); }// parseBaseMoveModifier() /** * Determines if a unit can transit from a location to this location. * <p> * Convenience method for more verbose canTransit() method. No arguments may * be null. */ public boolean canTransit(Phase phase, Order order) { return canTransit(order.getSource(), order.getSourceUnitType(), phase, order.getClass()); }// canTransit() /** * Determines if a unit can transit from a location to this location. * <p> * All defined border attributes have to match to prohibit border transit. * <p> * Null arguments are not permitted. */ public boolean canTransit(Location fromLoc, Unit.Type unit, Phase phase, Class orderClass) { /* System.out.println("border: "+id); System.out.println(" "+fromLoc.getProvince()+":"+fromLoc.getCoast()+", "+phase); */ // check from int nResults = 0; int failResults = 0; boolean fromMatched = false; if(from != null) { for(int i=0; i<from.length; i++) { if(from[i].equalsLoosely(fromLoc)) { fromMatched = true; break; } } } // we only apply criteria if 'from' was not specified, or // from was specified, and it matches. if(from == null || fromMatched) { // check unit type if(unitTypes != null) { nResults++; for(int i=0; i<unitTypes.length; i++) { if(unitTypes[i].equals(unit)) { failResults++; break; } } } // check order if(orderClasses != null) { nResults++; for(int i=0; i<orderClasses.length; i++) { if(orderClass == orderClasses[i]) { failResults++; break; } } } // check phase (season, phase, and year) if(seasons != null) { nResults++; for(int i=0; i<seasons.length; i++) { if(phase.getSeasonType().equals(seasons[i])) { failResults++; break; } } } if(phases != null) { nResults++; for(int i=0; i<phases.length; i++) { if(phase.getPhaseType().equals(phases[i])) { failResults++; break; } } } // we always check the year if(yearModifier != YEAR_NOT_SPECIFIED) { nResults++; final int theYear = phase.getYear(); if(yearModifier == YEAR_ODD) { failResults += ((theYear & 1) == 1) ? 1 : 0; } else if(yearModifier == YEAR_EVEN) { failResults += ((theYear & 1) == 1) ? 0 : 1; } else { failResults += ((yearMin <= theYear) && (theYear <= yearMax)) ? 1 : 0; } } } /* System.out.println(" fromMatched: "+fromMatched); System.out.println(" nResults: "+nResults); System.out.println(" failResults: "+failResults); */ // only return 'false' if EVERYTHING has failed, or, // nothing was tested assert (failResults <= nResults); return (failResults < nResults || nResults == 0); }// canTransit() /** Gets the base move modifier. Requires a non-null from location. */ public int getBaseMoveModifier(Location moveFrom) { if(from == null) { // if no locations defined, modifier is good for all locations. return baseMoveModifier; } else { for(int i=0; i<from.length; i++) { if(from[i].equalsLoosely(moveFrom)) { return baseMoveModifier; } } } // if not from the given location, no change in support. return 0; }// getBaseMoveModifier() /** Returns the description */ public String getDescription() { return description; }// getDescription() }// class Border