/** * Copyright (C) 2002-2012 The FreeCol Team * * This file is part of FreeCol. * * FreeCol 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. * * FreeCol 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 FreeCol. If not, see <http://www.gnu.org/licenses/>. */ package net.sf.freecol.server.ai.mission; import java.util.Random; import java.util.logging.Logger; import javax.xml.stream.XMLStreamException; import javax.xml.stream.XMLStreamReader; import javax.xml.stream.XMLStreamWriter; import net.sf.freecol.common.model.Ability; import net.sf.freecol.common.model.Colony; import net.sf.freecol.common.model.CombatModel; import net.sf.freecol.common.model.Europe; import net.sf.freecol.common.model.FreeColGameObject; import net.sf.freecol.common.model.Goods; import net.sf.freecol.common.model.GoodsContainer; import net.sf.freecol.common.model.GoodsType; import net.sf.freecol.common.model.Location; import net.sf.freecol.common.model.Map; import net.sf.freecol.common.model.Map.Direction; import net.sf.freecol.common.model.Ownable; import net.sf.freecol.common.model.PathNode; import net.sf.freecol.common.model.Player; import net.sf.freecol.common.model.Player.Stance; import net.sf.freecol.common.model.Settlement; import net.sf.freecol.common.model.Tension; import net.sf.freecol.common.model.Tile; import net.sf.freecol.common.model.Unit; import net.sf.freecol.common.model.Unit.MoveType; import net.sf.freecol.common.model.pathfinding.CostDecider; import net.sf.freecol.common.model.pathfinding.CostDeciders; import net.sf.freecol.common.model.pathfinding.GoalDecider; import net.sf.freecol.common.util.Utils; import net.sf.freecol.server.ai.AIColony; import net.sf.freecol.server.ai.AIGoods; import net.sf.freecol.server.ai.AIMain; import net.sf.freecol.server.ai.AIMessage; import net.sf.freecol.server.ai.AIObject; import net.sf.freecol.server.ai.AIPlayer; import net.sf.freecol.server.ai.AIUnit; import net.sf.freecol.server.ai.EuropeanAIPlayer; /** * A mission describes what a unit should do; attack, build colony, * wander etc. Every {@link AIUnit} should have a mission. By * extending this class, you create different missions. */ public abstract class Mission extends AIObject { private static final Logger logger = Logger.getLogger(Mission.class.getName()); /** A transport can be used.*/ protected static final int MINIMUM_TRANSPORT_PRIORITY = 60; /** Transport is required. */ protected static final int NORMAL_TRANSPORT_PRIORITY = 100; protected static final int NO_PATH_TO_TARGET = -2, NO_MORE_MOVES_LEFT = -1; // Common mission invalidity reasons. protected static final String AIUNITNULL = "aiUnit-null"; protected static final String TARGETNULL = "target-null"; protected static final String TARGETINVALID = "target-invalid"; protected static final String TARGETOWNERSHIP = "target-ownership"; protected static final String TARGETNOTFOUND = "target-not-found"; protected static final String UNITNOTAPERSON = "unit-not-a-person"; protected static final String UNITNOTOFFENSIVE = "unit-not-offensive"; protected static final String UNITNOTONMAP = "unit-not-on-map"; /** The unit to undertake the mission. */ private AIUnit aiUnit; /** * Creates an uninitialized mission. * * @param aiMain The main AI-object. */ public Mission(AIMain aiMain) { super(aiMain); this.aiUnit = null; } /** * Creates a mission for the given <code>AIUnit</code>. * * Note that missions are attached to their units, and thus do * not need AI ids, hence the plain superclass constructor. * * @param aiMain The main AI-object. * @param aiUnit The <code>AIUnit</code> this mission is created for. */ public Mission(AIMain aiMain, AIUnit aiUnit) { super(aiMain); this.aiUnit = aiUnit; } /** * Disposes this mission by removing any references to it. * Subclasses should override as needed. */ public void dispose() { // Nothing to do yet. } /** * Gets the AI-unit this mission has been created for. * * @return The <code>AIUnit</code>. */ public AIUnit getAIUnit() { return aiUnit; } /** * Sets the AI-unit this mission has been created for. * * @param aiUnit The <code>AIUnit</code>. */ protected void setAIUnit(AIUnit aiUnit) { this.aiUnit = aiUnit; } /** * Gets the unit this mission has been created for. * * @return The <code>Unit</code>. */ public Unit getUnit() { return (aiUnit == null) ? null : aiUnit.getUnit(); } /** * Convenience accessor for the owning player. * * @return The <code>Player</code> that owns the mission unit. */ protected Player getPlayer() { return (getUnit() == null) ? null : getUnit().getOwner(); } /** * Convenience accessor for the owning AI player. * * @return The <code>AIPlayer</code>. */ protected AIPlayer getAIPlayer() { return getAIMain().getAIPlayer(getUnit().getOwner()); } /** * Convenience accessor for the owning European AI player. * * @return The <code>EuropeanAIPlayer</code>. */ protected EuropeanAIPlayer getEuropeanAIPlayer() { Player player = getUnit().getOwner(); if (!player.isEuropean()) { throw new IllegalArgumentException("Not a European player: " + player); } return (EuropeanAIPlayer)getAIMain().getAIPlayer(player); } /** * Convenience accessor for the unit/player PRNG. * * @return A <code>Random</code> to use. */ protected Random getAIRandom() { return aiUnit.getAIRandom(); } /** * Is this mission valid? * * @return True if the mission is valid. */ public final boolean isValid() { return invalidReason() == null; } /** * Is an invalidity reason due to a target failure? * * @return True if the reason starts with "target-". */ public boolean isTargetReason(String reason) { return reason != null && reason.startsWith("target-"); } /** * Is a unit able to perform a mission of a particular type? * * @param unit The <code>Unit</code> to check. * @return A reason for mission invalidity, or null if none found. */ public static String invalidUnitReason(Unit unit) { return (unit == null) ? "unit-null" : (unit.isDisposed()) ? "unit-disposed" : (unit.isUnderRepair()) ? "unit-under-repair" : null; } /** * Is an AI unit able to perform a mission of a particular type? * * @param aiUnit The <code>AIUnit</code> to check. * @return A reason for mission invalidity, or null if none found. */ public static String invalidAIUnitReason(AIUnit aiUnit) { String reason; return (aiUnit == null) ? AIUNITNULL : ((reason = invalidUnitReason(aiUnit.getUnit())) != null) ? reason : null; } /** * Is an AI unable to perform a new mission because it already has * a valid, non-onetime mission? * * @return "mission-exists" if a valid mission is found, or null * if none found. */ public static String invalidNewMissionReason(AIUnit aiUnit) { return (aiUnit == null) ? AIUNITNULL : (aiUnit.hasMission() && !aiUnit.getMission().isOneTime() && aiUnit.getMission().isValid()) ? "mission-exists" : null; } /** * Is a target a valid mission target? * * @param target The target <code>Location</code> to check. * @return A reason for the target to be invalid, or null if none found. */ public static String invalidTargetReason(Location target) { return (target == null) ? Mission.TARGETNULL : (((FreeColGameObject)target).isDisposed()) ? "target-disposed" : null; } /** * Is a target a valid mission target? * * @param target The target <code>Location</code> to check. * @param player A <code>Player</code> that should own * the target. * @return A reason for the target to be invalid, or null if none found. */ public static String invalidTargetReason(Location target, Player owner) { String reason = invalidTargetReason(target); return (reason != null) ? reason : (target instanceof Ownable && owner != ((Ownable)target).getOwner()) ? Mission.TARGETOWNERSHIP : null; } /** * Is another player a valid attack target? * * @param aiUnit The <code>AIUnit</code> that will attack. * @param other The <code>Player</code> to attack. * @return A reason why the attack would be invalid, or null if none found. */ public static String invalidAttackReason(AIUnit aiUnit, Player other) { final Unit unit = aiUnit.getUnit(); final Player player = unit.getOwner(); return (player == other) ? Mission.TARGETOWNERSHIP : (player.isIndian() && player.getTension(other).getLevel() .compareTo(Tension.Level.CONTENT) <= 0) ? "target-native-tension-too-low" : (player.isEuropean() && !(player.getStance(other) == Stance.WAR || (unit.hasAbility(Ability.PIRACY) && player.getStance(other) != Stance.ALLIANCE))) ? "target-european-war-absent" : null; } /** * We have been blocked on the way to a target. Attack the blockage, * or just try to avoid it? * * @param aiUnit The <code>AIUnit</code> that was blocked. * @param target The target <code>Location</code>. * @return The blockage to attack, or null if not. */ public static Location resolveBlockage(AIUnit aiUnit, Location target) { final Unit unit = aiUnit.getUnit(); PathNode path = unit.findPath(target.getTile()); Direction d = null; if (path != null && path.next != null) { Tile tile = path.next.getTile(); Settlement settlement = tile.getSettlement(); Unit defender = tile.getDefendingUnit(unit); Location blocker = (settlement != null) ? settlement : unit; if (UnitSeekAndDestroyMission.invalidReason(aiUnit, blocker) == null) return blocker; } // Can not/decided not to attack. Take one random step in // roughly the right direction (if known). return null; } /** * Moves a unit one step randomly. * * @param logMe A string to log the random number generation with. * @param direction An optional preferred <code>Direction</code>. * @return The direction of the move, or null if no move was made. */ protected Direction moveRandomly(String logMe, Direction direction) { final Unit unit = getUnit(); if (unit.getMovesLeft() <= 0 || unit.getTile() == null) return null; if (logMe == null) logMe = "moveRandomly"; Random aiRandom = getAIRandom(); if (direction == null) { direction = Direction.getRandomDirection(logMe, aiRandom); } Direction[] directions = direction.getClosestDirections(logMe, aiRandom); for (int j = 0; j < directions.length; j++) { Direction d = directions[j]; Tile moveTo = unit.getTile().getNeighbourOrNull(d); if (moveTo != null && unit.getMoveType(d) == MoveType.MOVE) { return (AIMessage.askMove(aiUnit, d) && unit.getTile() == moveTo) ? d : null; // Failed! } } return null; // Stuck! } /** * Moves a unit randomly for the rest of its turn. * * @param logMe A string to log the random number generation with. */ protected void moveRandomlyTurn(String logMe) { Direction direction = null; while ((direction = moveRandomly(logMe, direction)) != null); getUnit().setMovesLeft(0); } /** * Move in a specified direction, but do not attack. * Always check the return from this in case the unit blundered into * a lost city and died. * The usual idiom is: "if (!moveButDontAttack(unit)) return;" * * @param direction The <code>Direction</code> to move. * @return True if the unit doing this mission is still valid/alive. */ protected boolean moveButDontAttack(Direction direction) { final Unit unit = getUnit(); if (direction != null && unit != null && unit.getMoveType(direction).isProgress()) { AIMessage.askMove(aiUnit, direction); } return getUnit() != null && !getUnit().isDisposed(); } /** * Finds the best existing settlement to use as a target. * Useful for missions where the unit might be in Europe, but should * go to a safe spot in the New World and proceed from there. * * @param player The <code>Player</code> that is searching. * @return A good settlement to restart a Mission from. */ protected static Settlement getBestSettlement(Player player) { int bestValue = -1; Settlement best = null; for (Settlement settlement : player.getSettlements()) { int value = settlement.getUnitCount() + settlement.getTile().getUnitCount(); if (settlement instanceof Colony) { Colony colony = (Colony)settlement; value += ((colony.isConnectedPort()) ? 10 : 0) // Favour coastal + colony.getAvailableWorkLocations().size(); } if (value > bestValue) { bestValue = value; best = settlement; } } return (best == null) ? null : best; } /** * A unit leaves a ship. * Fulfills a wish if possible. * * @param aiUnit The <code>AIUnit</code> to unload. * @param direction The <code>Direction</code> to move, if any. * @return True if the unit is unloaded. */ protected boolean unitLeavesTransport(AIUnit aiUnit, Direction direction) { final Unit carrier = getUnit(); final Unit unit = aiUnit.getUnit(); boolean result = (direction == null) ? AIMessage.askDisembark(aiUnit) : AIMessage.askMove(aiUnit, direction); Colony colony = unit.getColony(); if (result) result = unit.getLocation() != carrier; if (result && colony != null) { AIColony ac = getAIMain().getAIColony(colony); if (ac != null) ac.completeWish(unit); colony.firePropertyChange(Colony.REARRANGE_WORKERS, true, false); } return result; } /** * A unit joins a ship. * * @param aiUnit The <code>AIUnit</code> to load. * @param direction The <code>Direction</code> to move, if any. * @return True if the unit is loaded. */ protected boolean unitJoinsTransport(AIUnit aiUnit, Direction direction) { final Unit carrier = getUnit(); final Unit unit = aiUnit.getUnit(); Colony colony = unit.getColony(); boolean result = AIMessage.askEmbark(getAIUnit(), unit, direction); if (result) result = unit.getLocation() == carrier; if (result && colony != null) { colony.firePropertyChange(Colony.REARRANGE_WORKERS, true, false); } return result; } /** * Goods leaves a ship. * * @param type The <code>GoodsType</code> to unload. * @param amount The amount of goods to unload. * @return True if the unload succeeds. */ protected boolean goodsLeavesTransport(GoodsType type, int amount) { final Unit carrier = getUnit(); if (carrier.getGoodsCount(type) < amount) return false; final AIUnit aiUnit = getAIUnit(); Colony colony = carrier.getColony(); int oldAmount = carrier.getGoodsCount(type); Goods goods = new Goods(carrier.getGame(), carrier, type, amount); boolean result; if (carrier.isInEurope()) { if (carrier.getOwner().canTrade(type)) { result = AIMessage.askSellGoods(aiUnit, goods); logger.finest("Sell " + goods + " in Europe " + ((result) ? "succeeds" : "fails") + ": " + this); } else { // dump result = AIMessage.askUnloadCargo(aiUnit, goods); } } else { result = AIMessage.askUnloadCargo(aiUnit, goods); } if (result) { int newAmount = carrier.getGoodsCount(type); if (oldAmount - newAmount != amount) { logger.warning(carrier + " at " + carrier.getLocation() + " only unloaded " + (oldAmount - newAmount) + " " + type + " (" + amount + " expected)"); // TODO: sort this out. // For now, do not tolerate partial unloads. result = false; } } if (result && colony != null) { final AIColony aiColony = getAIMain().getAIColony(colony); if (aiColony != null) aiColony.completeWish(goods); colony.firePropertyChange(Colony.REARRANGE_WORKERS, true, false); } return result; } /** * Goods leaves a ship. * Completes a wish if possible. * * @param aiGoods The <code>AIGoods</code> to unload. * @return True if the unload succeeds. */ protected boolean goodsLeavesTransport(AIGoods aiGoods) { Goods goods = aiGoods.getGoods(); return goodsLeavesTransport(goods.getType(), goods.getAmount()); } /** * Goods joins a ship. * * @param aiGoods The <code>AIGoods</code> to load. * @return True if the load succeeds. */ protected boolean goodsJoinsTransport(AIGoods aiGoods) { final Unit carrier = getUnit(); final AIUnit aiUnit = getAIUnit(); final Goods goods = aiGoods.getGoods(); GoodsType goodsType = goods.getType(); int goodsAmount = goods.getAmount(); int oldAmount = carrier.getGoodsCount(goodsType); boolean result; if (carrier.isInEurope()) { result = AIMessage.askBuyGoods(aiUnit, goodsType, goodsAmount); } else { result = AIMessage.askLoadCargo(aiUnit, goods); } if (result) { int newAmount = carrier.getGoodsCount(goodsType); if (newAmount - oldAmount != goodsAmount) { logger.warning(carrier + " at " + carrier.getLocation() + " only loaded " + (newAmount - oldAmount) + " " + goodsType + " (" + goodsAmount + " expected)"); goodsAmount = newAmount - oldAmount; // TODO: sort this out. For now, tolerate partial loads. result = goodsAmount > 0; } } if (result) { Colony colony = carrier.getColony(); if (colony != null) { getAIMain().getAIColony(colony).removeAIGoods(aiGoods); } aiGoods.setGoods(new Goods(getGame(), carrier, goodsType, goodsAmount)); } return result; } // Deprecated, going away. protected void unitLeavesShip(AIUnit aiUnit) { unitLeavesTransport(aiUnit, null); } // Deprecated, going away. protected boolean sellCargoInEurope(Goods goods) { return goodsLeavesTransport(goods.getType(), goods.getAmount()); } // Deprecated, going away. protected boolean unloadCargoInColony(Goods goods) { return goodsLeavesTransport(goods.getType(), goods.getAmount()); } // Deprecated, going away. protected boolean moveUnitToEurope() { return getAIUnit().moveToEurope(); } // Deprecated, going away. protected boolean moveUnitToAmerica() { return getAIUnit().moveToAmerica(); } /** * Gets a standard mission-specific goal decider. * * @param aiUnit The <code>AIUnit</code> searching. * @param type The specific mission class. * @return A standard goal decider for the supplied mission type. */ public static GoalDecider getMissionGoalDecider(final AIUnit aiUnit, final Class type) { final AIPlayer aiPlayer = aiUnit.getAIMain() .getAIPlayer(aiUnit.getUnit().getOwner()); return new GoalDecider() { private int bestValue = -1; private PathNode best = null; public PathNode getGoal() { return best; } public boolean hasSubGoals() { return true; } public boolean check(Unit u, PathNode path) { int value = aiPlayer.scoreMission(aiUnit, path, type); if (value > bestValue) { bestValue = value; best = path; return true; } return false; } }; } /** * Evaluates a proposed mission type for a unit. * * This works out the basic mission-specific score. The final * result goes through further refinement in the fooAIPlayer * specialized scoreMission routines. * * TODO: see if we can remove the requirement this routine be * static (trouble is, we are trying to work out what sort of * mission to use, so there is not yet a mission object to call a * method of). We could use introspection to call a static * scoreTarget routine if it exists... * * @param aiUnit The <code>AIUnit</code> to perform the mission. * @param path A <code>PathNode</code> to the target of this mission. * @param type The mission class. * @return A score representing the desirability of this mission. */ public static int scorePath(AIUnit aiUnit, PathNode path, Class type) { return (type == BuildColonyMission.class) ? BuildColonyMission.scorePath(aiUnit, path) : (type == CashInTreasureTrainMission.class) ? CashInTreasureTrainMission.scorePath(aiUnit, path) : (type == DefendSettlementMission.class) ? DefendSettlementMission.scorePath(aiUnit, path) : (type == MissionaryMission.class) ? MissionaryMission.scorePath(aiUnit, path) : (type == PioneeringMission.class) ? PioneeringMission.scorePath(aiUnit, path) : (type == ScoutingMission.class) ? ScoutingMission.scorePath(aiUnit, path) : (type == UnitSeekAndDestroyMission.class) ? UnitSeekAndDestroyMission.scorePath(aiUnit, path) : -1; // NYI } /** * Finds a suitable seek-and-destroy target path for an AI unit. * * @param aiUnit The <code>AIUnit</code> to find a target for. * @param range An upper bound on the number of moves. * @param type The mission class. * @return A path to the target, or null if none found. */ public static PathNode findTargetPath(AIUnit aiUnit, int range, Class type) { Unit unit; Tile startTile; return (aiUnit == null || (unit = aiUnit.getUnit()) == null || unit.isDisposed() || (startTile = unit.getPathStartTile()) == null) ? null : unit.search(startTile, getMissionGoalDecider(aiUnit, type), CostDeciders.avoidIllegal(), range, unit.getCarrier()); } /** * Should the unit use transport to get to a specified tile? * * True if: * - The unit and tile are not null * - The unit is not there already * AND * - there is no path OR the path uses an existing carrier * * @param tile The <code>Tile</code> to go to. * @return True if the unit should use transport. */ protected boolean shouldTakeTransportToTile(Tile tile) { final Unit unit = getUnit(); PathNode path; return tile != null && unit != null && unit.getTile() != tile && ((path = unit.findPath(unit.getLocation(), tile, unit.getCarrier(), null)) == null || path.usesCarrier()); } /** * Tries to move this mission's unit to a target location. * * First check for units in transit, that is units on a carrier that * are going to but not yet in Europe, or going to but not yet at * a Tile and whose path still requires the carrier. These need to * be handled by the carrier's TransportMission, not by the unit's * Mission. * * Similarly check for units not in transit but should be, that * is units not on a carrier but can not get to their target * without one. These must just wait. * * If there is no impediment to the unit moving towards the target, * do so. Return an indicative MoveType for the result of the travel. * - MOVE if the unit has arrived at the target. The unit may not * have moved, or have exhausted its moves. * - MOVE_HIGH_SEAS if the unit has successfully set sail. * - MOVE_ILLEGAL if the unit is unable to proceed for now * - MOVE_NO_MOVES is underway but short of the target * - MOVE_NO_REPAIR if the unit died for whatever reason * - other legal results (e.g. ENTER_INDIAN_SETTLEMENT*) if that would * occur if the unit proceeded. Such moves require special handling * and are not performed here, the calling mission code must * handle them. * * Logging at `fine' on failures, `finest' on valid return values * other than normal path completion. * * @param logMe A prefix string for the log messages. * @param target The destination <code>Location</code>. * @param costDecider The <code>CostDecider</code> to use in any path * finding. * @return The type of move the unit stopped at. */ protected MoveType travelToTarget(String logMe, Location target, CostDecider costDecider) { final Tile targetTile = target.getTile(); if (!(target instanceof Europe) && targetTile == null) { throw new IllegalStateException("Target neither Europe nor Tile"); } final Unit unit = getUnit(); final AIUnit aiUnit = getAIUnit(); final Unit carrier = unit.getCarrier(); PathNode path = null; boolean inTransit = false; boolean needTransport = false; // Sanitize the unit location and drop out the trivial cases. if (unit.isAtSea()) { logger.finest(logMe + " at sea: " + this); return MoveType.MOVE_NO_MOVES; } else if (unit.isInEurope()) { if (target instanceof Europe) { if (unit.getLocation() instanceof Unit) { unitLeavesTransport(aiUnit, null); } return MoveType.MOVE; } else if (!unit.getOwner().canMoveToEurope()) { throw new IllegalStateException("Impossible move from Europe"); } } else if (unit.getTile() == null) { throw new IllegalStateException("Null unit tile: " + unit); } else { if (unit.getTile() == targetTile) { if (unit.getLocation() instanceof Unit) { unitLeavesTransport(aiUnit, null); } return MoveType.MOVE; } else if (target instanceof Europe) { if (!unit.getOwner().canMoveToEurope()) { logger.fine(logMe + " impossible move to Europe" + ": " + this); return MoveType.MOVE_ILLEGAL; } } } final Map map = unit.getGame().getMap(); if (target instanceof Europe) { // Going to Europe if (unit.getType().canMoveToHighSeas()) { if (unit.getTile().isDirectlyHighSeasConnected()) { if (AIMessage.askMoveTo(aiUnit, target)) { logger.finest(logMe + " sailed for " + target + ": " + this); return MoveType.MOVE_HIGH_SEAS; } else { logger.fine(logMe + " failed to sail for " + target + ": " + this); return MoveType.MOVE_ILLEGAL; } } path = unit.findPath(unit.getLocation(), target, null, costDecider); if (path == null) { logger.fine(logMe + " no path from " + unit.getLocation() + " to " + target + ": " + this); return MoveType.MOVE_ILLEGAL; } } else if (unit.isOnCarrier()) { inTransit = true; } else { needTransport = true; } } else if (unit.isInEurope()) { // Going to the map if (unit.getType().canMoveToHighSeas()) { unit.setDestination(target); if (AIMessage.askMoveTo(aiUnit, target)) { logger.finest(logMe + " sailed for " + target + ": " + this); return MoveType.MOVE_HIGH_SEAS; } else { logger.fine(logMe + " failed to sail for " + target + ": "+ this); return MoveType.MOVE_ILLEGAL; } } else if (unit.isOnCarrier()) { inTransit = true; } else { needTransport = true; } } else { // Moving on the map if (unit.getType().canMoveToHighSeas()) { // If there is no path for a high seas capable unit, give up. path = unit.findPath(unit.getTile(), targetTile, null, costDecider); if (path == null) { logger.fine(logMe + " no path from " + unit.getLocation() + " to " + target + ": " + this); return MoveType.MOVE_ILLEGAL; } } else if (unit.isOnCarrier()) { // Check if the carrier still has a useful path... path = unit.findPath(unit.getTile(), targetTile, unit.getCarrier(), costDecider); if (path == null) { logger.fine(logMe + " no transit from " + unit.getLocation() + " to " + target + ": " + this); return MoveType.MOVE_ILLEGAL; } // ...and whether the unit needs to stay on board. inTransit = path.isOnCarrier() && path.next.isOnCarrier(); } else { // Not high seas capable. If no path, it needs transport, // or is just blocked. path = unit.findPath(unit.getTile(), targetTile, null, costDecider); needTransport = path == null; } } if (inTransit) { logger.finest(logMe + " at " + unit.getLocation() + " in transit to " + target + ": " + this); return MoveType.MOVE_NO_MOVES; } else if (needTransport) { logger.finest(logMe + " at " + unit.getLocation() + " needs transport to " + target + ": " + this); return MoveType.MOVE_ILLEGAL; } else if (path == null) { throw new IllegalStateException("Path == null"); // Can not happen } else { Unit.MoveType mt = followPath(logMe, path); if (mt == MoveType.MOVE && unit.getLocation() instanceof Unit) { unitLeavesTransport(aiUnit, null); } return mt; } } /** * Follow a path to a target. * * Logging is fine on failures, finest on valid return values other * than normal path completion. * * @param logMe A prefix string for the log messages. * @param path The <code>PathNode</code> to follow. * @return The type of move the unit stopped at. */ protected MoveType followPath(String logMe, PathNode path) { final Unit unit = getUnit(); final AIUnit aiUnit = getAIUnit(); final int NO_EUROPE = 0; final int USES_EUROPE = 1; final int BOTH_EUROPE = 2; for (; path != null; path = path.next) { int useEurope = 0; // Sanitize the unit state. if (unit.isDisposed()) { logger.fine(logMe + " died going to " + path.getLocation() + ": " + this); return MoveType.MOVE_NO_REPAIR; } else if (unit.getMovesLeft() <= 0 || unit.isAtSea()) { logger.finest(logMe + " at " + unit.getLocation() + " en route to " + path.getLastNode().getLocation() + ": " + this); return MoveType.MOVE_NO_MOVES; } else if (unit.isInEurope()) { useEurope++; } else if (unit.getTile() == null) { logger.fine(logMe + " null location tile: " + this); return MoveType.MOVE_ILLEGAL; } // Sanitize the path node. if (path.getLocation() instanceof Europe) { useEurope++; } else if (path.getTile() == null) { logger.fine(logMe + " null path tile " + path.toString() + ": " + this); return MoveType.MOVE_ILLEGAL; } // Post-sanitization there are only two valid cases, // not using Europe at all, and sailing to/from Europe. // Ignore the trivial path within Europe. // On success, continue or return. // On failure, fall through to the error report. switch (useEurope) { case NO_EUROPE: if (unit.getTile() == path.getTile()) { if (unit.getLocation() instanceof Unit && !path.isOnCarrier()) { unitLeavesTransport(aiUnit, null); } continue; } MoveType mt = unit.getMoveType(path.getDirection()); if (!mt.isProgress()) { // Special handling required. logger.finest(logMe + " at " + unit.getTile() + " has special move " + mt + " to " + path.getTile() + ": " + this); return mt; } if (AIMessage.askMove(aiUnit, path.getDirection())) continue; break; case USES_EUROPE: if (AIMessage.askMoveTo(aiUnit, path.getLocation())) { logger.finest(logMe + " now on high seas: " + this); return MoveType.MOVE_HIGH_SEAS; } break; case BOTH_EUROPE: continue; // Do nothing, on to next node. default: throw new IllegalStateException("Can not happen"); } logger.finest(logMe + " at " + unit.getLocation() + " failed to move to " + path.getLocation() + ": " + this); return MoveType.MOVE_ILLEGAL; } return MoveType.MOVE; // Must have completed path normally, no log. } /** * If the unit in this mission is currently being transported, remove * it from the carrier's transport mission. * * @param reason The reason to remove the transportable. */ public void removeTransportable(String reason) { Unit u = getUnit(); AIUnit aiUnit = getAIUnit(); if (aiUnit != null && u.getLocation() instanceof Unit) { Unit carrier = (Unit)u.getLocation(); AIUnit aiCarrier = getAIMain().getAIUnit(carrier); if (aiCarrier != null) { Mission m = aiCarrier.getMission(); if (m instanceof TransportMission) { ((TransportMission)m).removeFromTransportList(aiUnit); } } } } // Fake implementation of Transportable interface. // Missions are not actually Transportables but the units that are // performing a mission delegate to these routines. /** * Gets the transport destination of the unit associated with this * mission. * TODO: is this still needed? * * @return The destination of a required transport or * <code>null</code> if no transport is needed. */ public Location getTransportDestination() { final Unit unit = getUnit(); final Unit carrier = unit.getCarrier(); PathNode path; return (unit.getTile() == null) ? ((unit.isOnCarrier()) ? carrier : unit).getFullEntryLocation() : (!unit.isOnCarrier()) ? null : (carrier.getSettlement() != null) ? carrier.getTile() : ((path = unit.findOurNearestSettlement()) == null) ? null : path.getLastNode().getTile(); } /** * Gets the priority of getting the unit to the transport * destination. * * @return The priority. */ public int getTransportPriority() { return (getTransportDestination() != null) ? NORMAL_TRANSPORT_PRIORITY : 0; } // Mission interface to be implemented/overridden by descendants. /** * Gets the target of this mission, if any. * * @return The target of this mission, or null if none. */ public abstract Location getTarget(); /** * Why is this mission invalid? * * Mission subclasses must implement this routine, which probably * should start by checking invalidAIUnitReason. * * A mission can be invalid for a number of subclass-specific * reasons. For example: a seek-and-destroy mission could be * invalid because of a improved stance towards the targeted * player. * * @return A reason for mission invalidity, or null if none found. */ public abstract String invalidReason(); /** * Is an AI unit able to perform a different mission? * * AIPlayers will call FooMission.invalidReason(aiUnit) to * determine whether it is valid to assign some unit to a * FooMission, so `interesting' Mission subclasses with complex * validity requirements must implement a routine with this * signature. Conversely, simple Missions that are always possible * need not. * * Implementations should usually start by calling this routine * (i.e. Mission.invalidReason(AIUnit)). * * @param aiUnit The <code>AIUnit</code> to check. * @return A reason for mission invalidity, or null if none found. */ public static String invalidReason(AIUnit aiUnit) { return invalidAIUnitReason(aiUnit); } /** * Is an AI unit able to perform a mission with a specified target? * * Specific Missions can be invalid for target-related reasons. * Such Missions need to implement a routine with this signature, * as it will be called by the GoalDeciders in map path find/searches * to choose a Mission target. * * Implementations should usually start by calling either * invalidAIUnitReason() or this routine if the target checking is * trivial. * * @param aiUnit The <code>AIUnit</code> to check. * @param loc The target <code>Location</code> to check. * @return A reason for mission invalidity, or null if none found. */ public static String invalidReason(AIUnit aiUnit, Location loc) { String reason = invalidAIUnitReason(aiUnit); return (reason != null) ? reason : invalidTargetReason(loc); } /** * Should this mission only be carried out once? * * Missions are not one-time by default, true one-time missions * must override this routine. * * @return False. */ public boolean isOneTime() { return false; } /** * Performs the mission. */ public abstract void doMission(); // Serialization /** * {@inheritDoc} */ protected void writeAttributes(XMLStreamWriter out) throws XMLStreamException { out.writeAttribute("unit", getUnit().getId()); } /** * {@inheritDoc} */ protected void readAttributes(XMLStreamReader in) throws XMLStreamException { String unit = in.getAttributeValue(null, "unit"); setAIUnit((AIUnit)getAIMain().getAIObject(unit)); } /** * {@inheritDoc} */ public String toString() { return Utils.lastPart(getClass().getName(), ".") + "@" + Integer.toString(hashCode()) + "-" + aiUnit; } }