// @(#)NJudgeOrderParser.java // // Copyright 2004 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.order.result; // package dip.order; import dip.order.result.Result; import dip.order.result.OrderResult; import dip.order.result.SubstitutedResult; import dip.order.result.DislodgedResult; import dip.misc.Utils; import dip.misc.Log; import dip.world.*; import java.util.StringTokenizer; import java.util.Collection; import java.util.Collections; import java.util.ArrayList; import java.util.LinkedList; import java.util.Iterator; import java.util.List; import java.util.regex.Pattern; import java.util.regex.Matcher; // for testing import dip.world.variant.VariantManager; import dip.world.variant.data.*; import java.io.*; /** * Parses nJudge-format orders into Orders and Results * <p> * This handles all 3 phases (Movement, Retreat, and Adjustment). * <p> * Please note that knowledge of the phase is required prior to * processing; for example, Retreat orders are used intead of Move * orders when the phase is PhaseType.RETREAT. Similarly, since * adjustment orders are in a format quite different from Movement * or Retreat phase orders, a similar hint PhaseType.ADJUSTMENT is * required. * <p> * Not all nJudge orders have a 1:1 mapping with jDip orders. * For example, jDip waive orders currently require a Province; * nJudge orders do not. Unusable waived builds and unusable pending * builds, as well as waived builds, are not converted to orders * but are denoted specifically in the returned NJudgeOrder. * An informative text Result describes this. * <p> * Note that DISLOGDGED order results do not have valid retreat locations * set. This cannot be done until further processing occurs. */ public class NJudgeOrderParser { // regexes for adjustment-phase processing // // general regex private static final String ADJUSTMENT_REGEX = "(?i)^([\\p{Alnum}\\-\\_]+):.*(remove|build|default).*\\s(army|fleet|wing).*\\s(?:in\\sthe|over\\sthe|in|over)\\s+((.+))"; // waived / unusable / pending regex private static final String ALTERNATE_ADJUSTMENT_REGEX = "(?i)^([\\p{Alnum}\\-\\_]+):\\s+(\\d*)\\s(unusable|unused)?.*(build).*((waived|pending)).*"; // order tokens private static final String ORDER_HOLD = "HOLD"; private static final String ORDER_MOVE = "->"; private static final String ORDER_SUPPORT = "SUPPORT"; private static final String ORDER_CONVOY = "CONVOY"; private static final String ORDER_DISBAND = "DISBAND"; private static final String ORDER_NO_ORDERS = "No"; // "England: Fleet Denmark, No Orders Processed." // all order tokens private static final String[] ORDER_NAME_TOKENS = { ORDER_HOLD, ORDER_MOVE, ORDER_SUPPORT, ORDER_CONVOY, ORDER_DISBAND, ORDER_NO_ORDERS }; // unit delimiter array private static final String[] UNIT_DELIMS = { "army", "fleet", "wing" }; // class variables private static final Pattern ADJUSTMENT_PATTERN = Pattern.compile(ADJUSTMENT_REGEX);; private static final Pattern ALTERNATE_ADJUSTMENT_PATTERN = Pattern.compile(ALTERNATE_ADJUSTMENT_REGEX);; // TEST harness /* public static void main(String args[]) throws OrderException { final String variantName = "Chaos"; World world = null; dip.world.Map map = null; // setup // get default variant directory. final String VARIANT_DIR = "variants"; File defaultVariantSearchDir = null; if(System.getProperty("user.dir") == null) { defaultVariantSearchDir = new File(".", VARIANT_DIR); } else { defaultVariantSearchDir = new File(System.getProperty("user.dir"), VARIANT_DIR ); } try { // parse variants VariantManager.init(new File[]{defaultVariantSearchDir}, false); // load the default variant (Standard) // error if it cannot be found!! Variant variant = VariantManager.getVariant(variantName, VariantManager.VERSION_NEWEST); if(variant == null) { System.out.println("ERROR: cannot find variant: "+variantName); System.exit(1); } // create the world world = WorldFactory.getInstance().createWorld(variant); map = world.getMap(); // set the RuleOptions in the World (this is normally done // by the GUI) world.setRuleOptions(RuleOptions.createFromVariant(variant)); } catch(Exception e) { System.out.println("ERROR: could not create variant."); System.out.println(e); e.printStackTrace(); System.exit(1); } // hold: OK // disband: OK // convoy: OK // support: OK // move: OK // ADJUST: String[] lines = { "2-Spa: Builds a fleet in Spain (south coast).", "F-Bul: Builds an army in Bulgaria.", "H-Den: Removes the fleet in the Norwegian Sea.", "F-Bul: Defaults, removing the fleet in the Black Sea.", "F-Bul: Defaults, removing the army in Siberia.", "2-Spa: Removes the wing over the St. Petersberg.", "F-Bul: Builds a wing in Sevastopol.", "F-Bul: 1 unusable build pending.", "2-Spa: 3 unusable builds pending.", "H-Den: 1 unused build pending.", "2-Spa: 2 unused builds pending.", "2-Spa: Build waived.", "F-Bul: 1 unusable build waived.", "2-Spa: 3 unusable builds waived." }; // MOVE //String line = "A-Ank: Army Ankara -> Smyrna. (*bounce, dislodged*)"; //String line = "1-Smy: Army Constantinople -> Aegean Sea -> Greece. (*bounce, dislodged*)"; // HOLD / DISBAND //String line = "N-Lvp: Fleet Irish Sea HOLD."; //String line = "T-Nap: Army Naples DISBAND."; // CONVOY //String line = "1-Smy: Fleet Ionian Sea CONVOY Army Bulgaria -> Serbia."; //String line = "N-Lvp: Fleet Mid-Atlantic Ocean CONVOY Q-Mar Army Brest -> Spain."; // SUPPORTS //String line = "B-Bel: Army Belgium SUPPORT K-Hol Army Holland."; //String line = "H-Den: Fleet Berlin SUPPORT Army Kiel."; //String line = "N-Lvp: Fleet North Sea SUPPORT Fleet Barents Sea -> Norwegian Sea."; //String line = "2-Spa: Fleet Spain (south coast) SUPPORT W-Por Fleet Portugal -> Mid-Atlantic Ocean."; NJudgeOrderParser njp = new NJudgeOrderParser(); for(int i=0; i<lines.length; i++) { System.out.println(">ORDER: "+lines[i]); NJudgeOrder njo = njp.parse(map, OrderFactory.getDefault(), Phase.PhaseType.ADJUSTMENT, lines[i]); System.out.println("> "+njo); } System.out.println("DONE:"); } */ /** Create an NJudgeOrderParser */ public NJudgeOrderParser() { }// NJudgeOrderParser() /** Class that holds an Order and Text Results */ public static class NJudgeOrder { private final Orderable order; private final List results; private final boolean isAdjustment; private final Power specialAdjustmentPower; private final boolean isWaive; private final int unusedPendingBuilds; private final int unusedPendingWaives; /** * Create an NJudgeOrder, which is an Orderable with * a dip.order.Result result(s). */ public NJudgeOrder(Orderable order, List results, boolean isAdjustmentPhase) { this(order, results, isAdjustmentPhase, null, false, 0, 0); }// NJudgeOrder() /** * Create an NJudgeOrder */ public NJudgeOrder(Orderable order, Result aResult, boolean isAdjustmentPhase) { this(order, createResultList(aResult), isAdjustmentPhase, null, false, 0, 0); }// NJudgeOrder() /** * Create an Adjustment-phase NJudgeOrder for unused pending Builds, * or pending Waives, but not both. * Also creates a Result for this. */ public NJudgeOrder(Power power, int unusedPendingBuilds, int unusedPendingWaives, Result result) { this(null, createResultList(result), true, power, false, unusedPendingBuilds, unusedPendingWaives); if( power == null || (unusedPendingBuilds > 0 && unusedPendingWaives > 0) || (unusedPendingBuilds < 0) || (unusedPendingWaives < 0) ) { throw new IllegalArgumentException(); } }// NJudgeOrder() /** * Create an Adjustment-phase NJudgeOrder for a Waived Build. * Also creates a Result for this. */ public NJudgeOrder(Power power, Result result) { this(null, createResultList(result), true, power, true, 0, 0); if(power == null) { throw new IllegalArgumentException(); } }// NJudgeOrder() /** * Create an NJudgeOrder */ private NJudgeOrder(Orderable order, List results, boolean isAdjustment, Power power, boolean isWaive, int unusedPendingBuilds, int unusedPendingWaives) { if(results == null) { throw new IllegalArgumentException(); } this.order = order; this.results = Collections.unmodifiableList(new ArrayList(results)); this.isAdjustment = isAdjustment; this.specialAdjustmentPower = power; this.isWaive = isWaive; this.unusedPendingBuilds = unusedPendingBuilds; this.unusedPendingWaives = unusedPendingWaives; }// NJudgeOrder() /** Returns the Order */ public Orderable getOrder() { return order; }// getOrder() /** * Returns the Results of the order. Each item in the list * is a subclass of dip.order.Result. */ public List getResults() { return results; }// getResults() /** Returns true if this is an PhaseType.ADJUSTMENT phase order. */ public boolean isAdjustmentPhase() { return isAdjustment; }// isAdjustmentPhase() /** * Returns the number of unused builds pending. This will be 0 * if there are no unsed builds pending. * <p> * Note that this applies only to ADJUSTMENT phase orders. This will * throw an IllegalStateException if <code>!isAdjustmentPhase()</code>. */ public int getUnusedPendingBuilds() { if(!isAdjustmentPhase()) { throw new IllegalStateException(); } return unusedPendingBuilds; }// getUnusedPendingBuilds() /** * Returns the number of unused builds waived. This will be 0 * if no unused builds were waived. * <p> * Note that this applies only to ADJUSTMENT phase orders. This will * throw an IllegalStateException if <code>!isAdjustmentPhase()</code>. */ public int getUnusedPendingWaives() { if(!isAdjustmentPhase()) { throw new IllegalStateException(); } return unusedPendingWaives; }// getUnusedPendingWaives() /** * Returns if a Build has been waived.. * Because nJudge Waive orders do not support a Province, * and jDip Waive orders require a Province, Waive orders are not * (yet) fully supported. * <p> * If this is NOT a waived build, this will return <code>false</code>. * <p> * This will be removed in a future version, when jDip-style Waive * orders are fully supported. * <p> * Note that this applies only to ADJUSTMENT phase orders. This will * throw an IllegalStateException if <code>!isAdjustmentPhase()</code>. */ public boolean isWaivedBuild() { if(!isAdjustmentPhase()) { throw new IllegalStateException(); } return isWaive; }// isWaivedBuild() /** * Returns the power for special adjustment orders, which are * obtained via the methods: * <ul> * <li><code>getUnusedPendingBuilds()</code></li> * <li><code>getUnusedPendingWaives()</code></li> * <li><code>isWaivedBuild()</code></li> * </ul> * This will throw an IllegalStateException() if we are not in the * adjustment phase. This will return <code>null</code> if we are * in the adjustment phase, but, none of isWaivedBuild() or * getUnusedXXX() methods have been set. */ public Power getAdjustmentPower() { if(!isAdjustmentPhase()) { throw new IllegalStateException(); } return specialAdjustmentPower; }// getAdjustmentPower() /** For debugging only */ public String toString() { StringBuffer sb = new StringBuffer(256); sb.append(this.getClass().getName()); sb.append("["); sb.append(getOrder()); sb.append(";"); if(results != null) { Iterator iter = results.iterator(); while(iter.hasNext()) { Result result = (Result) iter.next(); sb.append(result); if(iter.hasNext()) { sb.append(","); } } } sb.append(results); sb.append(",isAdjust="); sb.append(isAdjustment); sb.append(",adjPower="); sb.append(specialAdjustmentPower); sb.append(",waive="); sb.append(isWaive); sb.append(",unusedPendingBuilds="); sb.append(unusedPendingBuilds); sb.append(",unusedPendingWaives="); sb.append(unusedPendingWaives); sb.append("]"); return sb.toString(); } }// nested class NJudgeOrder /** * Parse a single line order. * <p> * Null arguments are not permitted, except for phaseType. * If phaseType is Phase.PhaseType.RETREAT, "Move" format orders will * be made into Retreat orders, and convoyed moves will be disallowed. */ public NJudgeOrder parse(final dip.world.Map map, final OrderFactory orderFactory, final Phase.PhaseType phaseType, final String line) throws OrderException { // create order parsing context final ParseContext pc = new ParseContext(map, orderFactory, phaseType, line); // parse results. This also removes the trailing '.' from the order final ArrayList resultList = new ArrayList(5); final String newOrderLine = removeTrailingDot( parseResults(pc, resultList) ); if(Phase.PhaseType.ADJUSTMENT.equals(phaseType)) { return parseAdjustmentOrder(pc, newOrderLine); } else { // tokenize order final String[] tokens = tokenize(newOrderLine); // parse order prefix OrderPrefix prefix = new OrderPrefix(pc, tokens); // parse predicate Orderable order = parsePredicate(pc, prefix, tokens); // parse text results into real results List results = createResults(pc, order, resultList); // create NJudgeOrder return new NJudgeOrder(order, results, pc.isAdjustmentPhase()); } }// parse() /** Tokenize input into whitespace-seperated strings. */ private String[] tokenize(final String input) { assert (input != null); return input.trim().split("\\s+"); }// tokenize() /** Creates a List with a single result */ private static List createResultList(Result aResult) { List list = new ArrayList(1); list.add(aResult); return list; }// createResultList() /** Parse the rest of the order */ private Orderable parsePredicate(final ParseContext pc, final OrderPrefix op, final String[] tokens) throws OrderException { final String type = op.orderName; if(type == ORDER_HOLD || type == ORDER_DISBAND || type == ORDER_NO_ORDERS) { return parseHoldOrDisband(pc, op, tokens, type); } else if(type == ORDER_MOVE) { return parseMove(pc, op, tokens); } else if(type == ORDER_SUPPORT) { return parseSupport(pc, op, tokens); } else if(type == ORDER_CONVOY) { return parseConvoy(pc, op, tokens); } throw new IllegalStateException("unknown orderName type"); }// parsePredicate() /** * All Orders begin with the following format: * <p> * POWER: UNIT LOCATION ORDERNAME * <p> * POWER: power name, followed by a ":" * UNIT: unit type (wing, army, fleet) * LOCATION: province name (may be multiple words), optionally * followed by a coast specifier * ORDERNAME: one of HOLD/SUPPORT/CONVOY/"->"/DISBAND * */ private class OrderPrefix { public final Power power; public final Unit.Type unit; public final Location location; public final String orderName; public final int tokenIndex; // index at which to continue parsing /** * Parses the Prefix of an Order using the * tokenized input string. * <p> * Throws an exception if a processing error occurs. */ public OrderPrefix(final ParseContext pc, final String[] tokens) throws OrderException { String tok = null; int idx = 0; // search and parse power name; power name must be followed by a ":" tok = getToken(pc, idx, tokens); if(!tok.endsWith(":") || tok.length() <= 1) { throw new OrderException("Improper Power format: \""+tok+"\" in order: "+pc.orderText); } final String powerNameText = tok.substring(0, tok.length()-1); this.power = pc.map.getClosestPower(powerNameText); if(this.power == null) { throw new OrderException("Unknown Power: \""+powerNameText+"\" in order: "+pc.orderText); } // search and parse unit type idx++; tok = getToken(pc, idx, tokens); this.unit = parseUnitType(pc, tok); // find the order name (order type) int tokIdx = idx; int orderNameIndex = -1; String tmpOrderName = null; while(tokIdx < tokens.length && orderNameIndex < 0) { final String aToken = tokens[tokIdx]; for(int onIdx = 0; onIdx < ORDER_NAME_TOKENS.length; onIdx++) { if(ORDER_NAME_TOKENS[onIdx].equals(aToken)) { // error-check orderNameIndex = tokIdx; tmpOrderName = ORDER_NAME_TOKENS[onIdx]; break; } } tokIdx++; } this.tokenIndex = orderNameIndex+1; this.orderName = tmpOrderName; if(orderNameIndex == -1) { throw new OrderException("Cannot determine order type for order: "+pc.orderText); } assert (this.orderName != null); // increment index (points to token AFTER unit type) idx++; // parse location this.location = parseLocation(pc, idx, orderNameIndex, tokens); }// parseOrderPrefix() }// OrderPrefix /** * Parse a Location (or throw an OrderException) between the * given start and end tokens. Start is inclusive, end is exclusive. * <p> * This will never return null. */ private Location parseLocation(final ParseContext pc, final int start, final int end, final String[] tokens) throws OrderException { // check args if(start < 0 || end < 0 || end > tokens.length || start > end) { throw new IllegalArgumentException("invalid start/end: "+start+","+end); } // parse location Name. This includes the province type (which may // be multiple tokens) and, optionally, the coast (also multiple // tokens). Periods are stripped. // StringBuffer sb = new StringBuffer(64); for(int i=start; i<end; i++) { if(i > start) { sb.append(' '); } sb.append(tokens[i]); } return parseLocation(pc, sb.toString()); }// parseLocation() /** * Parse a Location from the given text. * <p> * This will never return null. */ private Location parseLocation(final ParseContext pc, final String text) throws OrderException { final String replaceFrom[] = {".", ","}; final String replaceTo[] = {"", ""}; final String locationText = Coast.normalize( Utils.replaceAll(text, replaceFrom, replaceTo) ); final Location loc = pc.map.parseLocation(locationText); if(loc == null) { throw new OrderException("Invalid location \""+locationText+"\" in order: "+pc.orderText); } return loc; }// parseLocation() /** * Parse a Unit Type, throw an exception if not recognized. * Never returns null. */ private Unit.Type parseUnitType(final ParseContext pc, final String input) throws OrderException { final Unit.Type unitType = Unit.Type.parse(input); if(unitType == null) { throw new OrderException("Unknown Unit Type: \""+unitType+"\" in order: "+pc.orderText); } return unitType; }// parseUnitType() /** * Parse a Power. Throw an exception if not recognized. * Never returns null. */ private Power parsePower(final ParseContext pc, final String input) throws OrderException { final Power power = pc.map.getPower(input); if(power == null) { throw new OrderException("Unknown power: \""+input+"\" in order: "+pc.orderText); } return power; }// parseUnitType() /** * Parse a HOLD or DISBAND order/ * <p> * Format: [prefix] (HOLD || DISBAND). */ private Orderable parseHoldOrDisband(ParseContext pc, OrderPrefix op, final String[] tokens, final String type) throws OrderException { // NO additional parsing // if(type == ORDER_HOLD) { return pc.orderFactory.createHold(op.power, op.location, op.unit); } else if(type == ORDER_DISBAND) { return pc.orderFactory.createDisband(op.power, op.location, op.unit); } else if(type == ORDER_NO_ORDERS) { // FIXME: create own order type for "No Orders Processed" return pc.orderFactory.createHold(op.power, op.location, op.unit); } throw new IllegalStateException("expected HOLD or DISBAND"); }// parseHoldOrDisband() private Orderable parseMove(ParseContext pc, OrderPrefix op, final String[] tokens) throws OrderException { /* 3-StP: Army St Petersburg -> Moscow. (*bounce*) 1-Smy: Army Constantinople -> Aegean Sea -> Greece. keep parsing Locations until END or a -> is reached. so we keep looking for a -> until no more are found. if we have more than one, we'll add them to a list and then add that to the order Move order. */ LinkedList pathList = new LinkedList(); int idx = op.tokenIndex; int movTokIdx = findNextMoveToken(idx, tokens); while(movTokIdx != -1) { Location loc = parseLocation(pc, idx, movTokIdx, tokens); pathList.addLast(loc.getProvince()); idx = movTokIdx+1; movTokIdx = findNextMoveToken(idx, tokens); } // add last location final Location loc = parseLocation(pc, idx, tokens.length, tokens); pathList.addLast(loc.getProvince()); // create Move order if(pathList.size() == 1) { if(pc.isRetreatPhase()) { return pc.orderFactory.createRetreat(op.power, op.location, op.unit, loc); } else { return pc.orderFactory.createMove(op.power, op.location, op.unit, loc); } } else if(pathList.size() > 1) { if(pc.isRetreatPhase()) { throw new OrderException("Convoyed Retreat orders are not allowed. Order: "+pc.orderText); } // add source location at beginning of move list pathList.addFirst(op.location.getProvince()); final Province[] route = (Province[]) pathList.toArray( new Province[pathList.size()]); return pc.orderFactory.createMove(op.power, op.location, op.unit, loc, route); } else { // this probably will not occur.... throw new OrderException("Invalid movement path in Move order: "+pc.orderText); } }// parseMove() /** * Finds the next "->" (ORDER_MOVE) token index; returns -1 if * none is found. * */ private int findNextMoveToken(final int startIndex, final String[] tokens) { if(startIndex < 0) { throw new IllegalArgumentException(); } for(int i=startIndex; i<tokens.length; i++) { if(ORDER_MOVE.equals(tokens[i])) { return i; } } return -1; }// findNextMoveToken() private Orderable parseSupport(ParseContext pc, OrderPrefix op, final String[] tokens) throws OrderException { /* "B-Bel: Army Belgium SUPPORT K-Hol Army Holland."; "H-Den: Fleet Berlin SUPPORT Army Kiel."; "N-Lvp: Fleet North Sea SUPPORT Fleet Barents Sea -> Norwegian Sea."; "2-Spa: Fleet Spain (south coast) SUPPORT W-Por Fleet Portugal -> Mid-Atlantic Ocean."; if a "->" is NOT found, we are supporting a hold otherwise, supproting a move. POWER / UNITTYPE parsing as in CONVOY order. */ // token-index int idx = op.tokenIndex; Power supPower = null; // parse next token; may be a power (or power adjective), or null String[] toks = getTokenUpto(pc, idx, tokens, UNIT_DELIMS); if(toks != null) { // conjugate strings before parsing with getPower() StringBuffer sb = new StringBuffer(64); sb.append(toks[0]); for(int i=1; i<toks.length; i++) { sb.append(' '); sb.append(toks[i]); } final String supPowerName = sb.toString(); supPower = pc.map.getPower(supPowerName); // increment index appropriately idx += toks.length; // if toks is not null, we should have a valid power if(supPower == null) { throw new OrderException("Unrecognized Possesive Power \""+supPowerName+"\" in order: "+pc.orderText); } } // toks was null; thus, next token should be a unit. String tok = getToken(pc, idx, tokens); final Unit.Type supUnit = parseUnitType(pc, tok); // now parsing at token AFTER unit type token idx++; // now, find the "->" delimiter, if any. int delimIdx = -1; for(int i=idx; i<tokens.length; i++) { if(ORDER_MOVE.equals(tokens[i])) { delimIdx = i; break; } } // finish parsing order if(delimIdx == -1) { // support for a unit in place // // parse the support src final Location supSrc = parseLocation(pc, idx, tokens.length, tokens); // create the support order return pc.orderFactory.createSupport(op.power, op.location, op.unit, supSrc, supPower, supUnit); } else { // support for a moving unit // // parse the support src final Location supSrc = parseLocation(pc, idx, delimIdx, tokens); // parse the support dest, if any if(delimIdx+1 >= tokens.length) { throw new OrderException("Missing support destination in order: "+pc.orderText); } final Location supDest = parseLocation(pc, delimIdx+1, tokens.length, tokens); // create the support order return pc.orderFactory.createSupport(op.power, op.location, op.unit, supSrc, supPower, supUnit, supDest); } }// parseSupport() /** * parse a Convoy order: * PREFIX CONVOY [power] unit location -> location * remember, power is optional! */ private Orderable parseConvoy(ParseContext pc, OrderPrefix op, final String[] tokens) throws OrderException { /* "1-Smy: Fleet Ionian Sea CONVOY Army Bulgaria -> Serbia."; "N-Lvp: Fleet Mid-Atlantic Ocean CONVOY Q-Mar Army Brest -> Spain."; */ // token-index int idx = op.tokenIndex; Power convoyPower = null; // parse next token; may be a power (or power adjective), or null String[] toks = getTokenUpto(pc, idx, tokens, UNIT_DELIMS); if(toks != null) { // conjugate strings before parsing with getPower() StringBuffer sb = new StringBuffer(64); sb.append(toks[0]); for(int i=1; i<toks.length; i++) { sb.append(' '); sb.append(toks[i]); } final String convoyPowerName = sb.toString(); convoyPower = pc.map.getPower(convoyPowerName); // increment index appropriately idx += toks.length; // if toks is not null, we should have a valid power if(convoyPower == null) { throw new OrderException("Unrecognized Possesive Power \""+convoyPowerName+"\" in order: "+pc.orderText); } } // toks was null; thus, next token should be a unit. String tok = getToken(pc, idx, tokens); final Unit.Type convoyUnit = parseUnitType(pc, tok); // now parsing at token AFTER unit type token idx++; // now, find the "->" delimiter. There should be only one. int delimIdx = -1; for(int i=idx; i<tokens.length; i++) { if(ORDER_MOVE.equals(tokens[i])) { delimIdx = i; break; } } if(delimIdx == -1) { throw new OrderException("Missing \"->\" in Convoy order: "+pc.orderText); } // parse the convoy src final Location convoySrc = parseLocation(pc, idx, delimIdx, tokens); // parse the convoy dest if(delimIdx+1 >= tokens.length) { throw new OrderException("Missing convoy destination in order: "+pc.orderText); } final Location convoyDest = parseLocation(pc, delimIdx+1, tokens.length, tokens); // create the convoy order return pc.orderFactory.createConvoy(op.power, op.location, op.unit, convoySrc, convoyPower, convoyUnit, convoyDest); }// parseConvoy() /** * Checks to see if index is within bounds of token length. * If not, throws an OrderException. If so, return the * token at that index. */ private String getToken(final ParseContext pc, final int index, final String[] tokens) throws OrderException { if(index < 0) { throw new IllegalArgumentException(); } if(index >= tokens.length) { throw new OrderException("Truncated order: "+pc.orderText); } return tokens[index]; }// getToken() /** * Checks to see if index is within bounds of token length, * as getToken does. * <p> * If the delimiter is found, it will return all tokens upto * the delimiter. If the delimiter is found at the index, * 'null' will be returned. If no delimiter is found, an * exception is thrown. * */ private String[] getTokenUpto(final ParseContext pc, final int index, final String[] tokens, final String[] delim) throws OrderException { if(index < 0) { throw new IllegalArgumentException(); } if(index >= tokens.length) { throw new OrderException("Truncated order: "+pc.orderText); } // is delim at start? if so, return null. String tok = tokens[index]; for(int nDelim=0; nDelim<delim.length; nDelim++) { if(tok.equalsIgnoreCase(delim[nDelim])) { return null; } } ArrayList al = new ArrayList(3); al.add(tok); boolean foundDelim = false; for(int i=index+1; i<tokens.length; i++) { tok = tokens[i]; for(int nDelim=0; nDelim<delim.length; nDelim++) { if(tok.equalsIgnoreCase(delim[nDelim])) { foundDelim = true; break; } } if(foundDelim) { break; } else { al.add(tok); } } if(!foundDelim) { throw new OrderException("Truncated order: "+pc.orderText); } assert (!al.isEmpty()); return (String[]) al.toArray(new String[al.size()]); }// getTokenUpto() /** * Finds and removes all text between (and including) * the "(*" and "*)". * * <p> * Sets each result (comma-delim) in the given list. * <p> * Returns the cleaned-up order text. * */ private String parseResults(final ParseContext pc, final List results) throws OrderException { final String line = pc.orderText; final int rStart = line.indexOf("(*"); final int rEnd = line.indexOf("*)"); // no results. Return original order text. if(rStart == -1 && rEnd == -1) { results.clear(); return line; } // bad or missing (* or *) delimiters if(rEnd <= rStart || rStart == -1) { throw new OrderException("Invalid result \"(* *)\" delimiters for order: "+line); } final String resultText = line.substring(rStart+2, rEnd); final String[] resultStrings = resultText.split("\\s*,\\s*"); for(int i=0; i<resultStrings.length; i++) { results.add(resultStrings[i]); } // return the order, without the result text. return line.substring(0, rStart); }// parseResults() /** * Create proper Result objects from Move/Retreat phase * results. The supported result types are: * <ul> * <li>(no results) -> SUCCESS order result</li> * <li>bounce -> FAILURE w/bounce message</li> * <li>cut -> FAILURE w/cut message</li> * <li>void -> FAILURE w/voit message</li> * <li>dislodged -> DISLOGED result</li> * <li>?unknown? -> OrderException </li> * </ul> * The given results are returned in the List. */ private List createResults(ParseContext pc, Orderable order, final List stringResults) throws OrderException { List results = new ArrayList(stringResults.size()); Iterator iter = stringResults.iterator(); while(iter.hasNext()) { final String textResult = ((String) iter.next()).trim(); if(textResult.equalsIgnoreCase("bounce")) { results.add(new OrderResult(order, OrderResult.ResultType.FAILURE, "Bounce")); } else if(textResult.equalsIgnoreCase("cut")) { results.add(new OrderResult(order, OrderResult.ResultType.FAILURE, "Cut")); } else if(textResult.equalsIgnoreCase("no convoy")) { results.add(new OrderResult(order, OrderResult.ResultType.FAILURE, "No Convoy")); } else if(textResult.equalsIgnoreCase("dislodged")) { // create a failure result (if we were only dislodged) if(stringResults.size() == 1) { results.add(new OrderResult(order, OrderResult.ResultType.FAILURE, null)); } // create a TEMPORARY dislodged result here results.add(new OrderResult(order, OrderResult.ResultType.DISLODGED, "**TEMP**")); } else if(textResult.equalsIgnoreCase("destroyed")) { // create a failure result (if we were only dislodged) if(stringResults.size() == 1) { results.add(new OrderResult(order, OrderResult.ResultType.FAILURE, null)); } // destroyed result results.add(new DislodgedResult(order, null)); } else if(textResult.equalsIgnoreCase("void")) { results.add(new OrderResult(order, OrderResult.ResultType.VALIDATION_FAILURE, "Void")); } else { // unknown result type! Assume failure. throw new OrderException("Unknown result \""+textResult+"\" for order: "+pc.orderText); } } // if no result created, create a succes result. if(results.isEmpty()) { results.add(new OrderResult(order, OrderResult.ResultType.SUCCESS, null)); } return results; }// createResults() /** * Removes the trailing '.' from the order, and any leading * or trailing spaces. */ private String removeTrailingDot(final String input) { final String line = input.trim(); if(line.endsWith(".")) { return line.substring(0, line.length()-1); } return line; }// removeTrailingDot() private NJudgeOrder parseAdjustmentOrder(ParseContext pc, String line) throws OrderException { Matcher m = ADJUSTMENT_PATTERN.matcher(line); // attempt if(m.find()) { /* Groups: 1: Power 2: remove|build|default (default is also a remove!) 3: fleet|army|wing 4: location */ // parse power final Power power = parsePower(pc, m.group(1).trim()); // parse unit type final Unit.Type unit = parseUnitType(pc, m.group(3)); // parse location final Location location = parseLocation(pc, m.group(4).trim()); // parse action final boolean isDefault = ("default".equalsIgnoreCase(m.group(2))); Orderable order = null; if("build".equalsIgnoreCase(m.group(2))) { order = pc.orderFactory.createBuild(power, location, unit); } else { // remove or default order = pc.orderFactory.createRemove(power, location, unit); } NJudgeOrder njo = null; if(isDefault) { // power defaulted; order is contained in a substitutedResult SubstitutedResult substResult = new SubstitutedResult( null, order, "Power defaulted; unit removed."); njo = new NJudgeOrder(null, substResult, pc.isAdjustmentPhase()); } else { njo = new NJudgeOrder( order, new OrderResult(order, OrderResult.ResultType.SUCCESS, null), pc.isAdjustmentPhase() ); } assert (njo != null); return njo; } else { // reuse variable 'm' m = ALTERNATE_ADJUSTMENT_PATTERN.matcher(line); if(m.find()) { // parse power final Power power = parsePower(pc, m.group(1).trim()); // parse # (may be empty) // (if it is empty, set to 0) int numBuilds = 0; try { if(m.group(2).length() >= 1) { numBuilds = Integer.parseInt(m.group(2)); } } catch(NumberFormatException e) { throw new OrderException("Expected valid integer at \""+m.group(2)+"\" for order: "+pc.orderText); } // parse if unused/unusable boolean isUnusable = false; final String group3Tok = m.group(3); if( "unused".equalsIgnoreCase(group3Tok) || "unusable".equalsIgnoreCase(group3Tok)) { isUnusable = true; } // parse if pending/waived final boolean isPending = "pending".equalsIgnoreCase(m.group(5)); final boolean isWaived = "waived".equalsIgnoreCase(m.group(5)); if(m.group(3) == null) { // Group3 is null when it is a 'regular' waive order. Result result = new Result(power, "Build waived."); return new NJudgeOrder(power, result); } else { // since we depend on external input, these should // be exceptions rather than asserts // assert (isUnusable); if( !isUnusable ) { throw new IllegalStateException(); } assert (isPending != isWaived); if( isPending == isWaived ) { throw new IllegalStateException(); } if(isPending) { // pending builds Result result = new Result(power, String.valueOf(numBuilds)+" unusable build(s) pending."); return new NJudgeOrder(power, numBuilds, 0, result); } else { // waived builds Result result = new Result(power, String.valueOf(numBuilds)+" unusable build(s) waived."); return new NJudgeOrder(power, 0, numBuilds, result); } } } else { throw new OrderException("Cannot parse adjustment order: "+pc.orderText); } } }// parseAdjustmentOrder() /** Info needed by parsing methods */ private class ParseContext { public final dip.world.Map map; public final OrderFactory orderFactory; public final Phase.PhaseType phaseType; public final String orderText; public ParseContext(dip.world.Map map, OrderFactory orderFactory, Phase.PhaseType phaseType, String orderText) { this.map = map; this.orderFactory = orderFactory; this.phaseType = phaseType; this.orderText = orderText; } public boolean isRetreatPhase() { return (Phase.PhaseType.RETREAT.equals(phaseType)); }// isRetreatPhase() public boolean isAdjustmentPhase() { return (Phase.PhaseType.ADJUSTMENT.equals(phaseType)); }// isAdjustmentPhase() }// ParseContext() }// class NJudgeOrderParser