/**
* 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.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.FreeColGameObject;
import net.sf.freecol.common.model.Location;
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.Tile;
import net.sf.freecol.common.model.Unit;
import net.sf.freecol.common.model.Unit.UnitState;
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.model.pathfinding.GoalDeciders;
import net.sf.freecol.common.networking.NetworkConstants;
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.AIUnit;
import net.sf.freecol.server.ai.EuropeanAIPlayer;
import net.sf.freecol.server.ai.TileImprovementPlan;
/**
* Mission for controlling a pioneer.
*/
public class PioneeringMission extends Mission {
private static final Logger logger = Logger.getLogger(PioneeringMission.class.getName());
private static final String tag = "AI pioneer";
/**
* Maximum number of turns to travel to make progress on
* pioneering. This is low-ish because it is usually more
* efficient to ship the tools where they are needed and either
* create a new pioneer on site or send a hardy pioneer on
* horseback. The AI is probably smart enough to do the former
* already, and one day the latter.
*/
private static final int MAX_TURNS = 10;
/**
* Default distance in turns to a threatening unit.
*/
private static final int DEFAULT_THREAT_TURNS = 3;
/** The improvement this pioneer is to work on. */
private TileImprovementPlan tileImprovementPlan = null;
/**
* The target location to go to:
* - a tile where the tileImprovement is
* - a colony to go to to equip
* - just an initial colony to retarget from
*/
private Location target = null;
/**
* Creates a pioneering mission for the given <code>AIUnit</code>.
* Note that PioneeringMission.isValid(aiUnit) should be called
* before this, to guarantee that
* findTileImprovementPlan/findColonyWithTools succeed.
*
* @param aiMain The main AI-object.
* @param aiUnit The <code>AIUnit</code> this mission
* is created for.
*/
public PioneeringMission(AIMain aiMain, AIUnit aiUnit) {
super(aiMain, aiUnit);
setTarget(findTarget(aiUnit, true));
logger.finest(tag + " starts with target " + getTarget() + ": " + this);
uninitialized = false;
}
/**
* Creates a pioneering mission for the given <code>AIUnit</code>.
* Note that PioneeringMission.isValid(aiUnit) should be called
* before this, to guarantee that
* findTileImprovementPlan/findColonyWithTools succeed.
*
* @param aiMain The main AI-object.
* @param aiUnit The <code>AIUnit</code> this mission
* is created for.
* @param loc The target <code>Location</code>.
*/
public PioneeringMission(AIMain aiMain, AIUnit aiUnit, Location loc) {
super(aiMain, aiUnit);
setTarget(loc);
logger.finest(tag + " starts with target " + getTarget() + ": " + this);
uninitialized = false;
}
/**
* Creates a new <code>PioneeringMission</code> and reads the
* given element.
*
* @param aiMain The main AI-object.
* @param in The input stream containing the XML.
* @throws XMLStreamException if a problem was encountered
* during parsing.
* @see net.sf.freecol.server.ai.AIObject#readFromXML
*/
public PioneeringMission(AIMain aiMain, XMLStreamReader in)
throws XMLStreamException {
super(aiMain);
readFromXML(in);
uninitialized = getAIUnit() == null;
}
/**
* Get the best improvement associated with a tile.
*
* @return The <code>TileImprovementPlan</code>, or null if not found.
*/
private TileImprovementPlan getBestPlan(Tile tile) {
return getEuropeanAIPlayer().getBestPlan(tile);
}
/**
* Get the best improvement associated with a tile for a given unit.
* Take care to first check if the unit has a plan already, if so,
* return that.
*
* @return The <code>TileImprovementPlan</code>, or null if not found.
*/
private static TileImprovementPlan getBestPlan(AIUnit aiUnit, Tile tile) {
return ((EuropeanAIPlayer)aiUnit.getAIOwner()).getBestPlan(tile);
}
/**
* Sets the target for this mission, and the tile improvement plan
* as required.
*
* @param target The new target for this mission.
*/
public void setTarget(Location target) {
removeTransportable("retargeted");
this.target = target;
setTileImprovementPlan((target instanceof Tile)
? getBestPlan((Tile)target)
: null);
}
/**
* Gets the <code>TileImprovementPlan</code> for this mission.
*
* @return The <code>TileImprovementPlan</code>.
*/
public TileImprovementPlan getTileImprovementPlan() {
return tileImprovementPlan;
}
/**
* Sets the <code>TileImprovementPlan</code> which should
* be the next target.
*
* @param tip The <code>TileImprovementPlan</code>.
*/
public void setTileImprovementPlan(TileImprovementPlan tip) {
TileImprovementPlan old = tileImprovementPlan;
this.tileImprovementPlan = tip;
AIUnit aiUnit = getAIUnit();
if (old != tileImprovementPlan) {
if (old != null && old.getPioneer() == aiUnit) {
old.setPioneer(null);
}
if (tileImprovementPlan != null) {
tileImprovementPlan.setPioneer(aiUnit);
}
}
}
/**
* Abandons the current plan if any.
*/
private void abandonTileImprovementPlan() {
if (tileImprovementPlan != null) setTileImprovementPlan(null);
}
/**
* Disposes of this pioneering mission.
*/
public void dispose() {
abandonTileImprovementPlan();
super.dispose();
}
/**
* Does a supplied unit have tools?
*
* @param aiUnit The pioneer <code>AIUnit</code> to check.
* @return True if the pioneer has tools.
*/
private static boolean hasTools(AIUnit aiUnit) {
return aiUnit.getUnit().hasAbility("model.ability.improveTerrain");
}
/**
* Does this pioneer have tools?
*
* @return True if the pioneer has tools.
*/
private boolean hasTools() {
return hasTools(getAIUnit());
}
/**
* Extract a valid target for this mission from a path.
*
* @param aiUnit A <code>AIUnit</code> to perform the mission.
* @param path A <code>PathNode</code> to extract a target from.
* @return A target for this mission, or null if none found.
*/
public static Location extractTarget(AIUnit aiUnit, PathNode path) {
if (path == null) return null;
final Location loc = path.getLastNode().getLocation();
return (loc == null) ? null
: ((hasTools(aiUnit))
? ((invalidReason(aiUnit, loc.getTile()) != null) ? null
: loc.getTile())
: ((invalidReason(aiUnit, loc.getColony()) != null) ? null
: loc.getColony()));
}
/**
* Evaluate a potential pioneering mission for a given unit and
* path.
*
* @param aiUnit The <code>AIUnit</code> to do the mission.
* @param path A <code>PathNode</code> to take to the target.
* @return A score for the proposed mission.
*/
public static int scorePath(AIUnit aiUnit, PathNode path) {
final Location loc = extractTarget(aiUnit, path);
if (hasTools(aiUnit)) {
TileImprovementPlan tip;
if (loc instanceof Tile
&& (tip = getBestPlan(aiUnit, (Tile)loc)) != null) {
return 1000 * tip.getValue() / (path.getTotalTurns() + 1);
}
} else {
if (loc instanceof Colony) {
return 1000 / (path.getTotalTurns() + 1);
}
}
return Integer.MIN_VALUE;
}
/**
* Makes a goal decider that checks pioneering sites.
*
* @param aiUnit The <code>AIUnit</code> to search with.
* @param deferOK Keep track of the nearest colonies to use as a
* fallback destination.
* @return A suitable <code>GoalDecider</code>.
*/
private static GoalDecider getGoalDecider(final AIUnit aiUnit,
final boolean deferOK) {
final Player owner = aiUnit.getUnit().getOwner();
final GoalDecider gd = new GoalDecider() {
private PathNode bestPath = null;
private int bestValue = 0;
public PathNode getGoal() { return bestPath; }
public boolean hasSubGoals() { return true; }
public boolean check(Unit u, PathNode path) {
int value = scorePath(aiUnit, path);
if (bestValue < value) {
bestValue = value;
bestPath = path;
return true;
}
return false;
}
};
return (deferOK) ? GoalDeciders.getComposedGoalDecider(gd,
GoalDeciders.getOurClosestSettlementGoalDecider())
: gd;
}
/**
* Finds a suitable pioneering target for the supplied unit.
*
* @param aiUnit The <code>AIUnit</code> to test.
* @param deferOK If true, allow the search to return a nearby existing
* colony as a temporary target.
* @return A <code>PathNode</code> to the target, or null if none found.
*/
public static PathNode findTargetPath(AIUnit aiUnit, boolean deferOK) {
if (invalidAIUnitReason(aiUnit) != null) return null;
final Unit unit = aiUnit.getUnit();
final Tile startTile = unit.getPathStartTile();
if (startTile == null) return null;
PathNode path;
final Unit carrier = unit.getCarrier();
final GoalDecider gd = getGoalDecider(aiUnit, deferOK);
final CostDecider standardCd
= CostDeciders.avoidSettlementsAndBlockingUnits();
final CostDecider relaxedCd = CostDeciders.numberOfTiles();
// Try for something sensible nearby.
path = unit.search(startTile, gd, standardCd, MAX_TURNS, carrier);
if (path != null) return path;
// One more try with a relaxed cost decider and no range limit.
return unit.search(startTile, gd, relaxedCd, INFINITY, carrier);
}
/**
* Gets the Colony that most needs a pioneer.
*
* @param aiUnit The pioneer <code>AIUnit</code>.
* @return The colony with the most outstanding tile improvement plans.
*/
private static Colony getBestPioneeringColony(AIUnit aiUnit) {
EuropeanAIPlayer aiPlayer = (EuropeanAIPlayer)aiUnit.getAIOwner();
AIColony bestColony = null;
int bestValue = -1;
for (AIColony aic : aiPlayer.getAIColonies()) {
int value = aic.getTileImprovementPlans().size();
if (value > bestValue) {
bestValue = value;
bestColony = aic;
}
}
if (bestColony == null) return null;
Colony colony = bestColony.getColony();
if (colony.isConnectedPort()) return colony;
PathNode path = aiUnit.getUnit().findOurNearestPort();
return (path == null) ? colony
: path.getLastNode().getLocation().getColony();
}
/**
* Finds a suitable pioneering target for the supplied unit.
* Falls back to the best settlement if the unit is not on the map.
*
* @param aiUnit The <code>AIUnit</code> to test.
* @param deferOK Enables deferring to a fallback colony.
* @return A target for this mission.
*/
public static Location findTarget(AIUnit aiUnit, boolean deferOK) {
PathNode path = findTargetPath(aiUnit, deferOK);
return (path != null) ? extractTarget(aiUnit, path)
: (deferOK) ? getBestPioneeringColony(aiUnit)
: null;
}
/**
* Prepare a unit for this mission.
*
* @param aiUnit The <code>AIUnit</code> to prepare.
* @return A reason why the unit can not perform this mission, or null
* if none.
*/
public static String prepare(AIUnit aiUnit) {
String reason = invalidReason(aiUnit);
if (reason != null) return reason;
final Unit unit = aiUnit.getUnit();
if (!hasTools(aiUnit)) {
aiUnit.equipForRole(Unit.Role.PIONEER, false);
}
return (hasTools(aiUnit) || unit.hasAbility(Ability.EXPERT_PIONEER))
? null
: "unit-missing-tools";
}
// Fake Transportable interface.
/**
* Gets the transport destination for units with this mission.
*
* @return The destination for this <code>Transportable</code>.
*/
@Override
public Location getTransportDestination() {
return (getTarget() == null
|| !shouldTakeTransportToTile(getTarget().getTile())) ? null
: getTarget();
}
// Mission interface
/**
* Gets the target for this mission, either the colony to go to,
* or the tile that needs improvement.
*
* @return The target for this mission.
*/
public Location getTarget() {
return target;
}
/**
* Why would a PioneeringMission be invalid with the given unit.
*
* @param aiUnit The <code>AIUnit</code> to check.
* @return A reason why the mission would be invalid with the unit,
* or null if none found.
*/
private static String invalidMissionReason(AIUnit aiUnit) {
String reason = invalidAIUnitReason(aiUnit);
return (reason != null) ? reason
: (!aiUnit.getUnit().isPerson()) ? Mission.UNITNOTAPERSON
: null;
}
/**
* Why would a PioneeringMission be invalid with the given unit and colony.
*
* @param aiUnit The <code>AIUnit</code> to check.
* @param colony The <code>Colony</code> to check.
* @return A reason why the mission would be invalid, or null if
* none found.
*/
private static String invalidColonyReason(AIUnit aiUnit, Colony colony) {
String reason = invalidTargetReason(colony,
aiUnit.getUnit().getOwner());
return (reason != null)
? reason
: (!hasTools(aiUnit)
&& !colony.canProvideEquipment(Unit.Role.PIONEER
.getRoleEquipment(colony.getSpecification())))
? "colony-can-not-provide-equipment"
: null;
}
/**
* Gets the existing tile improvement plan for a unit and tile.
*
* @param aiUnit The <code>AIUnit</code> to check.
* @param tile The <code>Tile</code> to check.
* @return The associated <code>TileImprovementPlan</code>.
*/
private static TileImprovementPlan getPlan(AIUnit aiUnit, Tile tile) {
if (aiUnit.getMission() instanceof PioneeringMission) {
PioneeringMission pm = (PioneeringMission)aiUnit.getMission();
if (pm.getTileImprovementPlan() != null
&& pm.getTileImprovementPlan().getTarget() == tile) {
return pm.getTileImprovementPlan();
}
}
return null;
}
/**
* Why would a PioneeringMission be invalid with the given unit and tile.
*
* @param aiUnit The <code>AIUnit</code> to check.
* @param tile The <code>Tile</code> to check.
* @return A reason why the mission would be invalid, or null if
* none found.
*/
private static String invalidTileReason(AIUnit aiUnit, Tile tile) {
return (tile == null) ? Mission.TARGETINVALID
: (!hasTools(aiUnit)) ? "unit-needs-tools"
: (getPlan(aiUnit, tile) == null
&& getBestPlan(aiUnit, tile) == null) ? "tile-has-no-plan"
: null;
}
/**
* Why is this mission invalid?
*
* @return A reason for mission invalidity, or null if none found.
*/
public String invalidReason() {
return invalidReason(getAIUnit(), getTarget());
}
/**
* Why would this mission be invalid with the given AI unit?
*
* @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 invalidMissionReason(aiUnit);
}
/**
* Why would this mission be invalid with the given AI unit and location?
*
* @param aiUnit The <code>AIUnit</code> to check.
* @param loc The <code>Location</code> to check.
* @return A reason for invalidity, or null if none found.
*/
public static String invalidReason(AIUnit aiUnit, Location loc) {
String reason = invalidMissionReason(aiUnit);
return (reason != null)
? reason
: (loc instanceof Tile)
? invalidTileReason(aiUnit, (Tile)loc)
: (loc instanceof Colony)
? ((aiUnit.getUnit().getLocation() instanceof Tile)
? invalidColonyReason(aiUnit, (Colony)loc)
: invalidTargetReason((Colony)loc, aiUnit.getUnit().getOwner()))
: Mission.TARGETINVALID;
}
// Not a one-time mission, omit isOneTime().
/**
* Performs this mission.
*
* - Gets tools if needed.
* - Makes sure we have a valid plan.
* - Get to the target.
* - Claim it if necessary.
* - Make the improvement.
*/
public void doMission() {
String reason = invalidReason();
Location newTarget;
if (isTargetReason(reason)) {
if ((newTarget = findTarget(getAIUnit(), false)) == null) {
logger.finest(tag + " unable to retarget: " + this);
return;
}
setTarget(newTarget);
} else if (reason != null) {
logger.finest(tag + " broken(" + reason + "): " + this);
return;
}
final AIUnit aiUnit = getAIUnit();
final Unit unit = getUnit();
final Player player = unit.getOwner();
final EuropeanAIPlayer aiPlayer = getEuropeanAIPlayer();
final CostDecider costDecider
= CostDeciders.avoidSettlementsAndBlockingUnits();
Tile tile;
String where;
while (!hasTools()) { // Get tools first.
// Go there and clear target on arrival.
if (travelToTarget(tag, getTarget(), costDecider)
!= Unit.MoveType.MOVE) return;
where = ((Colony)getTarget()).getName();
setTarget(null);
// Try to equip
if (aiUnit.equipForRole(Unit.Role.PIONEER, false)
&& hasTools()) {
newTarget = findTarget(aiUnit, false);
logger.finest(tag + " reached " + where
+ " and equips, retargeting " + newTarget + ": " + this);
} else {
newTarget = findTarget(aiUnit, false);
logger.finest(tag + " reached " + where
+ " but fails to equip, retargeting: " + newTarget
+ ": " + this);
}
if (newTarget == null) return;
setTarget(newTarget);
}
// Going to an intermediate colony?
if (getTarget() instanceof Colony
&& invalidTargetReason(getTarget(), player) == null) {
if (travelToTarget(tag, getTarget(), costDecider)
!= Unit.MoveType.MOVE) return;
where = ((Colony)getTarget()).getName();
newTarget = findTarget(aiUnit, false);
logger.finest(tag + " reached intermediate colony " + where
+ ", retargeting " + newTarget + ": " + this);
if (newTarget == null) return;
setTarget(newTarget);
}
// Now insist on a tip-target.
if (tileImprovementPlan != null
&& !aiPlayer.validateTileImprovementPlan(tileImprovementPlan)) {
setTarget(null);
}
if (tileImprovementPlan == null) {
newTarget = findTarget(aiUnit, false);
if (newTarget != null) setTarget(newTarget);
if (newTarget == null || tileImprovementPlan == null) {
logger.finest(tag + " at " + unit.getLocation()
+ " could not find improvement: " + this);
return;
}
logger.finest(tag + " retargeting " + newTarget
+ "/" + tileImprovementPlan + ": " + this);
}
// Go there.
Unit.MoveType mt = travelToTarget(tag, getTarget(), costDecider);
switch (mt) {
case MOVE_NO_MOVES:
return;
case MOVE_ILLEGAL:
// Might be a temporary blockage due to an occupying unit
// at the target. Move randomly and retry if adjacent.
Direction d = unit.getTile().getDirection(getTarget().getTile());
if (d != null) moveRandomly(tag, d);
return;
case MOVE:
break;
default:
logger.warning(tag + " unexpected move type (" + mt
+ ") at " + unit.getLocation() + ": " + this);
return;
}
// Take control of the land before proceeding to improve.
tile = getTarget().getTile();
if (!player.owns(tile)) {
// TODO: Better choice whether to pay or steal.
// Currently always pay if we can, steal if we can not.
boolean fail = false;
int price = player.getLandPrice(tile);
if (price < 0) {
fail = true;
} else {
if (price > 0 && !player.checkGold(price)) {
price = NetworkConstants.STEAL_LAND;
}
if (!AIMessage.askClaimLand(tile, aiUnit, price)
|| !player.owns(tile)) { // Failed to take ownership
fail = true;
}
}
if (fail) {
aiPlayer.removeTileImprovementPlan(tileImprovementPlan);
tileImprovementPlan.dispose();
setTarget(null);
logger.finest(tag + " can not claim land at " + tile
+ ": " + this);
return;
}
}
// Check for threats
int turnsNeeded = DEFAULT_THREAT_TURNS;
if (unit.getWorkImprovement() != null) {
turnsNeeded = Math.min(turnsNeeded, unit.getWorkLeft());
}
if (unit.isInDanger(turnsNeeded, 0.25f)) {
PathNode safe = unit.findOurNearestSettlement(false, 1, false);
if (safe != null) {
travelToTarget(tag + " (evading)",
safe.getLastNode().getTile(), costDecider);
}
return;
}
// Work on the improvement
if (unit.getState() == UnitState.IMPROVING) {
unit.setMovesLeft(0);
logger.finest(tag + " improving "
+ tileImprovementPlan.getType() + ": " + this);
} else if (unit.checkSetState(UnitState.IMPROVING)) {
if (AIMessage.askChangeWorkImprovementType(aiUnit,
tileImprovementPlan.getType())) {
logger.finest(tag + " began improvement "
+ tileImprovementPlan.getType()
+ " at target " + tile + ": " + this);
} else {
aiPlayer.removeTileImprovementPlan(tileImprovementPlan);
tileImprovementPlan.dispose();
setTarget(null);
logger.warning(tag + " failed to improve " + tile
+ ": " + this);
}
} else { // Probably just out of moves.
logger.finest(tag + " waiting to improve at " + tile
+ ": " + this);
}
}
// Serialization
/**
* Writes all of the <code>AIObject</code>s and other AI-related
* information to an XML-stream.
*
* @param out The target stream.
* @throws XMLStreamException if there are any problems writing to the
* stream.
*/
protected void toXMLImpl(XMLStreamWriter out) throws XMLStreamException {
toXML(out, getXMLElementTagName());
}
/**
* {@inheritDoc}
*/
protected void writeAttributes(XMLStreamWriter out)
throws XMLStreamException {
super.writeAttributes(out);
if (target != null) {
writeAttribute(out, "target", (FreeColGameObject)target);
}
}
/**
* {@inheritDoc}
*/
protected void readAttributes(XMLStreamReader in)
throws XMLStreamException {
super.readAttributes(in);
// Do not use setTarget in serialization
String str = in.getAttributeValue(null, "target");
target = (str == null) ? null : getGame().getFreeColLocation(str);
}
/**
* Returns the tag name of the root element representing this object.
*
* @return "pioneeringMission".
*/
public static String getXMLElementTagName() {
return "pioneeringMission";
}
}