/** * 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 org.freecolandroid.xml.stream.XMLStreamException; import org.freecolandroid.xml.stream.XMLStreamReader; import org.freecolandroid.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.Goods; 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.PathNode; import net.sf.freecol.common.model.Player; 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.CostDeciders; import net.sf.freecol.common.model.pathfinding.GoalDecider; import net.sf.freecol.common.networking.Connection; import net.sf.freecol.server.ai.AIColony; 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.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()); protected static final int MINIMUM_TRANSPORT_PRIORITY = 60, // A transport can be used NORMAL_TRANSPORT_PRIORITY = 100; // Transport is required protected static final int NO_PATH_TO_TARGET = -2, NO_MORE_MOVES_LEFT = -1; private AIUnit aiUnit; /** * Creates a mission. * @param aiMain The main AI-object. */ public Mission(AIMain aiMain) { this(aiMain, null); } /** * Creates a mission for the given <code>AIUnit</code>. * * @param aiMain The main AI-object. * @param aiUnit The <code>AIUnit</code> this mission * is created for. * @exception NullPointerException if <code>aiUnit == null</code>. */ public Mission(AIMain aiMain, AIUnit aiUnit) { super(aiMain); this.aiUnit = aiUnit; } /** * Disposes this mission by removing any references to it. */ public void dispose() { // Nothing to do yet. } /** * Gets the unit this mission has been created for. * * @return The <code>Unit</code>. */ public Unit getUnit() { return aiUnit.getUnit(); } /** * 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; } /** * Convenience accessor for the unit/player PRNG. * * @return A <code>Random</code> to use. */ protected Random getAIRandom() { return aiUnit.getAIRandom(); } /** * Convenience accessor for the owning European AI player. * * @return The <code>EuropeanAIPlayer</code>. */ protected EuropeanAIPlayer getEuropeanAIPlayer() { return (EuropeanAIPlayer)getAIMain().getAIPlayer(getUnit().getOwner()); } /** * Moves the unit owning this mission towards the given * <code>Tile</code>. This is done in a loop until the tile is * reached, there are no moves left, the path to the target cannot * be found or that the next step is not a move. * * @param tile The <code>Tile</code> the unit should move towards. * @return The direction to take the final move or null if the * move can not be made. */ protected Direction moveTowards(Tile tile) { PathNode pathNode = getUnit().findPath(tile); return (pathNode == null) ? null : moveTowards(pathNode); } /** * Moves the unit owning this mission using the given path. This * is done in a loop until the end of the path is reached, the * next step is not a move or when there are no moves left. * * @param pathNode The first node of the path. * @return The direction to continue moving the path or null if the * move can not be made. */ protected Direction moveTowards(PathNode pathNode) { if (getUnit().getMovesLeft() <= 0) return null; while (pathNode.next != null && pathNode.getTurns() == 0) { if (!isValid()) return null; if (!getUnit().getMoveType(pathNode.getDirection()).isProgress()) { break; } if (!AIMessage.askMove(aiUnit, pathNode.getDirection()) || getUnit() == null || getUnit().isDisposed()) { return null; } pathNode = pathNode.next; } return (pathNode.getTurns() == 0 && getUnit().getMoveType(pathNode.getDirection()).isLegal()) ? pathNode.getDirection() : null; } /** * Move a unit randomly. */ protected void moveRandomly() { Unit unit = getUnit(); Direction[] randomDirections = Direction.getRandomDirections("moveRandomly", getAIRandom()); while (isValid() && unit.getMovesLeft() > 0) { Tile thisTile = getUnit().getTile(); for (int j = 0; j < randomDirections.length; j++) { Direction direction = randomDirections[j]; if (thisTile.getNeighbourOrNull(direction) != null && unit.getMoveType(direction) == MoveType.MOVE) { AIMessage.askMove(aiUnit, direction); break; } } unit.setMovesLeft(0); } } protected void moveUnitToAmerica() { AIMessage.askMoveTo(aiUnit, getUnit().getOwner().getGame().getMap()); } protected void moveUnitToEurope() { AIMessage.askMoveTo(aiUnit, getUnit().getOwner().getEurope()); } /** * 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 target to attack within the given range. * * @param maxTurns The maximum number of turns the unit is allowed * to spend in order to reach the target. * @return The path to the target or <code>null</code> if no target can * be found. */ protected PathNode findTarget(int maxTurns) { if (!getUnit().isOffensiveUnit()) { throw new IllegalStateException("A target can only be found for offensive units. You tried with: " + getUnit().toString()); } GoalDecider gd = new GoalDecider() { private PathNode bestTarget = null; private int higherTension = 0; public PathNode getGoal() { return bestTarget; } public boolean hasSubGoals() { return true; } public boolean check(Unit unit, PathNode pathNode) { CombatModel combatModel = getGame().getCombatModel(); Tile newTile = pathNode.getTile(); Unit defender = newTile.getDefendingUnit(unit); if( defender == null){ return false; } if( defender.getOwner() == unit.getOwner()){ return false; } if (newTile.isLand() && unit.isNaval() || !newTile.isLand() && !unit.isNaval()) { return false; } int tension = unit.getOwner().getTension(defender.getOwner()).getValue(); if (unit.getIndianSettlement() != null && unit.getIndianSettlement().hasContactedSettlement(defender.getOwner())) { tension += unit.getIndianSettlement().getAlarm(defender.getOwner()).getValue(); } if (defender.canCarryTreasure()) { tension += Math.min(defender.getTreasureAmount() / 10, 600); } if (defender.getType().getDefence() > 0 && newTile.getSettlement() == null) { tension += 100 - combatModel.getDefencePower(unit, defender) * 2; } if (defender.hasAbility(Ability.EXPERT_SOLDIER) && !defender.isArmed()) { tension += 50 - combatModel.getDefencePower(unit, defender) * 2; } if (unit.hasAbility(Ability.PIRACY)){ tension += PrivateerMission.getModifierValueForTarget(combatModel, unit, defender); } // TODO-AI-CHEATING: REMOVE WHEN THE AI KNOWNS HOW TO HANDLE PEACE WITH THE INDIANS: if (unit.getOwner().isIndian() && defender.getOwner().isAI()) { tension -= 200; } // END: TODO-AI-CHEATING if (tension > Tension.Level.CONTENT.getLimit()) { if (bestTarget == null) { bestTarget = pathNode; higherTension = tension; return true; } else if (bestTarget.getTurns() == pathNode.getTurns() && tension > higherTension) { bestTarget = pathNode; higherTension = tension; return true; } } return false; } }; return getUnit().search(getUnit().getTile(), gd, CostDeciders.avoidIllegal(), maxTurns, null); } /** * Find the nearest reachable settlement to a unit (owned by the * same player) excepting the any on the current tile. * * @param unit The <code>Unit</code> to check. * @return The nearest settlement if any, otherwise null. */ protected PathNode findNearestOtherSettlement(Unit unit) { Player player = unit.getOwner(); PathNode nearest = null; int dist = Integer.MAX_VALUE; for (Settlement settlement : player.getSettlements()) { if (settlement.getTile() == unit.getTile()) return null; PathNode path = unit.findPath(settlement.getTile()); if (path != null) { int d = path.getTotalTurns(); if (d < dist) { nearest = path; dist = d; } } } return nearest; } /** * Unload a unit. * * @param aiUnit The <code>AIUnit</code> to unload. * @return True if the unit is unloaded. */ protected boolean unitLeavesShip(AIUnit aiUnit) { Colony colony = aiUnit.getUnit().getColony(); if (colony != null) { AIColony ac = getAIMain().getAIColony(colony); if (ac != null) ac.completeWish(aiUnit.getUnit()); colony.firePropertyChange(Colony.REARRANGE_WORKERS, true, false); } return AIMessage.askDisembark(aiUnit); } /** * Unload some goods in a colony. * * @param goods The <code>Goods</code> to unload. * @return True if the goods are unloaded. */ protected boolean unloadCargoInColony(Goods goods) { Colony colony = aiUnit.getUnit().getColony(); AIColony ac; if (colony != null && (ac = getAIMain().getAIColony(colony)) != null) { ac.completeWish(goods); } return AIMessage.askUnloadCargo(aiUnit, goods); } /** * Sell some goods in Europe. * * @param goods The <code>Goods</code> to sell. * @return True if the goods are sold. */ protected boolean sellCargoInEurope(Goods goods) { // CHEAT: Remove when the AI is good enough Player p = getUnit().getOwner(); if (p.isAI() && getAIMain().getFreeColServer().isSingleplayer()) { // Double the income by adding this bonus: p.modifyGold(p.getMarket().getSalePrice(goods)); } return AIMessage.askSellGoods(aiUnit, goods); } /** * Should the unit use transport to get to a specified tile? * * True if: * - The unit is not there already * AND * - the unit already has transport, this will always be faster * (TODO: actually, mounted units on good roads might be faster, * check for this) * - if not on the map * - if on the map but can not find a path to the tile, unless * adjacent to the destination which usually means the path * finding failed due to a temporary blockage such as an enemy unit * - if the path to the tile will take more than MAX_TURNS * * @param tile The <code>Tile</code> to go to. * @return True if the unit should use transport. */ protected boolean shouldTakeTransportToTile(Tile tile) { final int MAX_TURNS = 5; final Unit unit = getUnit(); PathNode path; return tile != null && unit != null && unit.getTile() != tile && (unit.isOnCarrier() || unit.getTile() == null || ((path = unit.findPath(tile)) == null && !unit.getTile().isAdjacent(tile)) || (path != null && path.getTotalTurns() >= MAX_TURNS)); } /** * Gets a suitable tile to start path searches from for a unit. * * Must handle all the cases where the unit is off the map, and * take account of the use of a carrier. * * If the unit is in or heading to Europe, return null because there * is no good way to tell where the unit will reappear on the map, * which is in question anyway if not on a carrier. * * @param unit The <code>Unit</code> to check. * @return A suitable starting tile, or null if none found. */ protected static Tile getPathStartTile(Unit unit) { Location loc; if (unit.isOnCarrier()) { final Unit carrier = (Unit)unit.getLocation(); if (carrier.getTile() != null) { return carrier.getTile(); } else if (carrier.getDestination() instanceof Map) { return carrier.getFullEntryLocation(); } else if (carrier.getDestination() instanceof Colony) { return ((Colony)carrier.getDestination()).getTile(); } } else if (unit.getTile() != null) { return unit.getTile(); } // Must be heading to or in Europe. return null; } /** * 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. * * 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. * * 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_NO_MOVES if out of moves short of the target * - MOVE_ILLEGAL if the unit is unable to proceed for now * - MOVE_NO_REPAIR if the unit died for whatever reason * - other 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. * * @param logMe A prefix string for the log messages. * @param target The destination <code>Location</code>. * @return The type of move the unit stopped at. */ protected MoveType travelToTarget(String logMe, Location target) { final Tile targetTile = target.getTile(); if (!(target instanceof Europe) && targetTile == null) { throw new IllegalStateException("Target neither Europe nor Tile"); } final Unit unit = getUnit(); final Unit carrier = (unit.isOnCarrier()) ? (Unit)unit.getLocation() : null; PathNode path = null; boolean inTransit = false; boolean needTransport = false; if (target instanceof Europe) { if (unit.isInEurope()) return MoveType.MOVE; if (unit.isOnCarrier()) { inTransit = true; } else { needTransport = true; } } else { if (unit.getTile() == targetTile) return MoveType.MOVE; if (unit.isOnCarrier()) { if (carrier.getTile() == null) { inTransit = true; } else { path = unit.findPath(unit.getTile(), targetTile, carrier); if (path == null) { logger.finest(logMe + " can not get from " + unit.getTile() + " to " + targetTile + ": " + unit); return MoveType.MOVE_ILLEGAL; } else { inTransit = path.isOnCarrier(); } } } else { if (unit.isInEurope()) { needTransport = true; } else if (unit.getTile() == null) { // Can not happen throw new IllegalStateException("No tile or carrier: " + unit); } else { path = unit.findPath(targetTile); if (path == null) needTransport = true; } } } if (inTransit) { logger.finest(logMe + " in transit to " + target + ": " + unit); return MoveType.MOVE_ILLEGAL; } else if (needTransport) { logger.finest(logMe + " needs transport to " + target + ": " + unit); return MoveType.MOVE_ILLEGAL; } // This can not happen. if (path == null) throw new IllegalStateException("Path == null"); // Follow the path towards the target. for (; path != null; path = path.next) { if (unit.getMovesLeft() <= 0) { logger.finest(logMe + " en route to " + targetTile + ": " + unit); return MoveType.MOVE_NO_MOVES; } MoveType mt = unit.getMoveType(path.getDirection()); if (!mt.isProgress()) return mt; // Special handling required if (!AIMessage.askMove(aiUnit, path.getDirection())) { logger.finest(logMe + " move failed at " + unit.getTile() + ": " + unit); return MoveType.MOVE_ILLEGAL; } else if (unit.isDisposed()) { logger.finest(logMe + " died en route to " + targetTile + ": " + unit); return MoveType.MOVE_NO_REPAIR; } } return MoveType.MOVE; // Must have completed path } // 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. * * @return The destination of a required transport or * <code>null</code> if no transport is needed. */ public Location getTransportDestination() { final Unit unit = getUnit(); if (unit.getTile() == null) { return ((unit.isOnCarrier()) ? ((Unit)unit.getLocation()) : unit) .getFullEntryLocation(); } if (!unit.isOnCarrier()) return null; final Unit carrier = (Unit)unit.getLocation(); if (carrier.getSettlement() != null) return carrier.getTile(); // Find the closest friendly Settlement: GoalDecider gd = new GoalDecider() { private PathNode bestTarget = null; public PathNode getGoal() { return bestTarget; } public boolean hasSubGoals() { return false; } public boolean check(Unit unit, PathNode pathNode) { Tile newTile = pathNode.getTile(); boolean hasOurSettlement = (newTile.getSettlement() != null) && newTile.getSettlement().getOwner() == unit.getOwner(); if (hasOurSettlement) bestTarget = pathNode; return hasOurSettlement; } }; PathNode path = carrier.search(carrier.getTile(), gd, CostDeciders.avoidSettlementsAndBlockingUnits(), Integer.MAX_VALUE, null); return (path == 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. /** * Checks if this mission is valid for the given unit. * * @param aiUnit The <code>AIUnit</code> to check. * @return True if the unit can be usefully assigned this mission. */ public static boolean isValid(AIUnit aiUnit) { return aiUnit != null && aiUnit.getMission() == null && aiUnit.getUnit() != null && !aiUnit.getUnit().isDisposed() && !aiUnit.getUnit().isUnderRepair(); } /** * Checks if this mission is still valid to perform. At this * level, if the unit was killed then the mission becomes invalid. * * A mission can be invalidated for a number of subclass-specific * reasons. For example: a seek-and-destroy mission could be * invalidated when the relationship towards the targeted player * improves. * * @return True if the unit is still intact to perform its mission. */ public boolean isValid() { return getUnit() != null && !getUnit().isDisposed(); } /** * Checks if this Mission should 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. Must be implemented by the subclasses. * * @param connection The <code>Connection</code> to the server. */ public abstract void doMission(Connection connection); /** * Gets debugging information about this mission. This string is * a short representation of this object's state. * * @return "". */ public String getDebuggingInfo() { return ""; } // Serialization /** * {@inherit-doc} */ protected void writeAttributes(XMLStreamWriter out) throws XMLStreamException { out.writeAttribute("unit", getUnit().getId()); } /** * {@inherit-doc} */ protected void readAttributes(XMLStreamReader in) throws XMLStreamException { String unit = in.getAttributeValue(null, "unit"); setAIUnit((AIUnit)getAIMain().getAIObject(unit)); } }