/**
* 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));
}
}