/**
* 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 org.freecolandroid.xml.stream.XMLStreamException;
import org.freecolandroid.xml.stream.XMLStreamReader;
import org.freecolandroid.xml.stream.XMLStreamWriter;
import net.sf.freecol.common.model.Colony;
import net.sf.freecol.common.model.Location;
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.pathfinding.CostDeciders;
import net.sf.freecol.common.model.pathfinding.GoalDecider;
import net.sf.freecol.common.networking.Connection;
import net.sf.freecol.common.networking.NetworkConstants;
import net.sf.freecol.common.util.Utils;
import net.sf.freecol.server.ai.AIMain;
import net.sf.freecol.server.ai.AIMessage;
import net.sf.freecol.server.ai.AIUnit;
import org.w3c.dom.Element;
/**
* Mission for building a <code>Colony</code>.
*
* This mission can be used in two different ways:
* <ul>
* <li>Build a colony at a specific location.</li>
* <li>Find a site for a colony and build it there.</li>
* </ul>
*
* This mission will be aborted in the former case if the value gets
* below a given threshold, while a colony will always get built (if
* there is sufficient space on the map) in the latter case. Use the
* appropriate constructor to get the desired behaviour.
*
* @see net.sf.freecol.common.model.Colony Colony
*/
public class BuildColonyMission extends Mission {
private static final Logger logger = Logger.getLogger(BuildColonyMission.class.getName());
/** The maximum number of turns to travel to a building site. */
private static final int MAX_TURNS = 5;
/**
* The <code>Tile</code> where the <code>Colony</code> should be built.
* Alternately, this can be the site of an existing colony--- the unit
* is to go there and then retarget a new site.
*
* This target must be non-null for this mission to remain valid.
* doMission() maintains this as much as possible.
*/
private Tile target = null;
/** The value of the target <code>Tile</code>. */
private int colonyValue = -1;
/**
* 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.
* @param target The target <code>Tile</code> to build a colony at.
*/
public BuildColonyMission(AIMain aiMain, AIUnit aiUnit, Tile target) {
super(aiMain, aiUnit);
this.target = target;
colonyValue = (target == null || target.getColony() != null) ? -1
: aiUnit.getUnit().getOwner().getColonyValue(target);
logger.finest("AI colony builder starts with target " + target
+ " and value " + colonyValue
+ ": " + aiUnit.getUnit());
}
/**
* Creates a new <code>BuildColonyMission</code>.
*
* @param aiMain The main AI-object.
* @param element An <code>Element</code> containing an XML-representation
* of this object.
*/
public BuildColonyMission(AIMain aiMain, Element element) {
super(aiMain);
readFromXMLElement(element);
}
/**
* Creates a new <code>BuildColonyMission</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 BuildColonyMission(AIMain aiMain, XMLStreamReader in)
throws XMLStreamException {
super(aiMain);
readFromXML(in);
}
/**
* Gets the target of this mission.
*
* @return The tile where a colony is to be built.
*/
public Tile getTarget() {
return target;
}
/**
* Gets the value of a path to a colony building site. The value is
* proportional to the general desirability of the site, and inversely
* proportional to the number of turns to get there.
*
* @param path The <code>PathNode</code> to check.
* @param player The <code>Player</code> that will found the colony,
* needed to check that the site can be acquired.
* @return A score for the building site.
*/
private static float getSiteValue(PathNode path, Player player) {
final Tile tile = path.getTile();
if (tile == null || !tile.isLand()) return -1.0f;
float turns = path.getTotalTurns() + 1.0f;
// Can the player acquire the tile?
switch (player.canClaimToFoundSettlementReason(tile)) {
case NONE:
break;
case NATIVES:
// Penalize value when the tile will need to be stolen
int price = player.getLandPrice(tile);
if (price > 0 && !player.checkGold(price)) turns *= 2.0f;
break;
default:
return -1.0f;
}
return player.getColonyValue(tile) / turns;
}
/**
* Finds the best existing colony to use as a target.
* When the unit gets there, then retarget.
* This encourages the colonies to clump.
*
* @param player The <code>Player</code> that is searching.
* @return A good colony to restart from.
*/
private static Tile getBestColonyTile(Player player) {
int bestValue = -1;
Colony best = null;
for (Colony colony : player.getColonies()) {
int value = ((colony.isConnected()) ? 10 : 0) // Favour coastal
+ colony.getUnitCount()
+ colony.getAvailableWorkLocations().size();
if (value > bestValue) {
bestValue = value;
best = colony;
}
}
return (best == null) ? null : best.getTile();
}
/**
* Makes a goal decider that checks colony sites.
*
* @param deferOK Keep track of the nearest of our colonies, to use
* as a fallback destination.
* @return A suitable <code>GoalDecider</code>.
*/
private static GoalDecider getColonyDecider(final boolean deferOK) {
return new GoalDecider() {
private PathNode best = null;
private float bestValue = 0.0f;
private PathNode backup = null;
private int backupTurns = INFINITY;
public PathNode getGoal() {
return (best != null) ? best : backup;
}
public boolean hasSubGoals() { return true; }
public boolean check(Unit u, PathNode path) {
Colony colony = path.getTile().getColony();
if (colony != null) {
if (deferOK
&& colony.getOwner() == u.getOwner()
&& colony.isConnected()
&& path.getTotalTurns() < backupTurns) {
backupTurns = path.getTotalTurns();
backup = path;
}
return false;
}
float value = getSiteValue(path, u.getOwner());
if (value > bestValue) {
bestValue = value;
best = path;
return true;
}
return false;
}
};
}
/**
* Finds a site for a new colony. Favour closer sites.
*
* @param unit The <code>Unit</code> to find a colony site with.
* @param deferOK If true, allow the search to return a nearby existing
* colony as a temporary target.
* @return A suitable tile for a new colony, or backup target.
*/
public static Tile findTargetTile(AIUnit aiUnit, boolean deferOK) {
final Unit unit = aiUnit.getUnit();
if (unit == null || unit.isDisposed()) return null;
final Tile startTile = getPathStartTile(unit);
final Player player = unit.getOwner();
if (startTile == null) return getBestColonyTile(player);
PathNode path;
final Unit carrier = (unit.isOnCarrier()) ? ((Unit)unit.getLocation())
: null;
final GoalDecider colonyDecider = getColonyDecider(deferOK);
if ((path = unit.search(startTile, colonyDecider,
CostDeciders.avoidIllegal(), MAX_TURNS,
carrier)) != null) return path.getLastNode().getTile();
// Retry, but increase the range.
if ((path = unit.search(startTile, colonyDecider,
CostDeciders.avoidIllegal(), MAX_TURNS * 3,
carrier)) != null) return path.getLastNode().getTile();
// One more try with a relaxed cost decider and no range limit.
if ((path = unit.search(startTile, colonyDecider,
CostDeciders.numberOfTiles(), INFINITY,
carrier)) != null) return path.getLastNode().getTile();
// Enough.
return null;
}
// Fake Transportable interface
/**
* Gets the transport destination for the unit with this mission.
*
* @return The destination for this <code>Transportable</code>.
*/
public Location getTransportDestination() {
return (shouldTakeTransportToTile(target)) ? target : null;
}
// Mission interface
/**
* Checks if this mission is valid for the given unit.
*
* @param aiUnit The <code>AIUnit</code> to test.
* @return True if the unit can be usefully assigned this mission.
*/
public static boolean isValid(AIUnit aiUnit) {
if (!Mission.isValid(aiUnit)) return false;
return aiUnit.getUnit().hasAbility("model.ability.foundColony");
}
/**
* Checks if this mission is still valid to perform.
*
* @return True if this mission is still valid to perform.
*/
public boolean isValid() {
return super.isValid()
&& getUnit().hasAbility("model.ability.foundColony")
&& target != null;
}
/**
* Performs this mission.
*
* @param connection The <code>Connection</code> to the server.
*/
public void doMission(Connection connection) {
// Check the target
final Unit unit = getUnit();
final Player player = unit.getOwner();
final AIUnit aiUnit = getAIUnit();
int value;
if (target == null
|| (value = player.getColonyValue(target)) < colonyValue
|| (target.getColony() != null
&& target.getColony().getOwner() != player)) {
target = findTargetTile(aiUnit, true);
if (target == null) {
logger.finest("AI colony builder could not find a target: "
+ unit);
return;
}
colonyValue = player.getColonyValue(target);
}
if (travelToTarget("AI colony builder", target)
!= Unit.MoveType.MOVE) return;
// If arrived at one of our colonies it is time to either
// retarget or just join the current colony. Only continue
// looking for a building site if `few colonies', but insist on
// finding a building site this time.
if (target.getColony() != null) {
String name = unit.getTile().getColony().getName();
if (getEuropeanAIPlayer().hasFewColonies()) {
target = findTargetTile(aiUnit, false);
logger.finest("AI colony builder arrived at " + name
+ ", retargeting " + target
+ ": " + unit);
} else {
logger.finest("AI colony builder gives up and joins " + name
+ ": " + unit);
target = null;
}
return;
}
// Arrived at the target (non-colony) tile.
if (target.getOwner() == null) {
; // All is well
} else if (player.owns(target)) { // Already ours, clear users
Colony colony = (Colony) target.getOwningSettlement();
if (colony != null
&& colony.getColonyTile(target) != null) {
colony.getColonyTile(target).relocateWorkers();
}
} else {
// Not our tile, so claim it first. Fail if someone has
// claimed the tile and will not sell. Otherwise try to
// buy it (TODO: remove cheat) or steal it.
int price = player.getLandPrice(target);
boolean fail = price < 0;
if (price > 0 && !player.checkGold(price)) {
if (Utils.randomInt(logger, "Cheat gold",
getAIRandom(), 4) == 0) {
// CHEAT: provide the gold needed
player.modifyGold(price);
}
}
if (price >= 0) {
fail = !AIMessage.askClaimLand(connection, target, null,
((price == 0) ? 0 : (player.checkGold(price)) ? price
: NetworkConstants.STEAL_LAND))
|| !player.owns(target);
}
if (fail) {
logger.finest("AI colony builder failed to claim land at "
+ unit.getTile() + ": " + unit);
target = null;
return;
}
}
// Check that the unit has moves left, which are required for building.
if (unit.getMovesLeft() <= 0) {
logger.finest("AI colony builder waiting to build at " + target
+ ": " + unit);
return;
}
// Clear to build the colony.
if (AIMessage.askBuildColony(aiUnit, Player.ASSIGN_SETTLEMENT_NAME)
&& target.getColony() != null) {
aiUnit.setMission(new WorkInsideColonyMission(getAIMain(), aiUnit,
getAIMain().getAIColony(target.getColony())));
logger.finest("AI colony builder"
+ " built " + target.getColony().getName() + ": " + unit);
} else {
logger.warning("AI colony builder failed to build at " + target
+ ": " + unit);
}
target = null;
}
/**
* Gets debugging information about this mission. This string is a short
* representation of this object's state.
*
* @return The <code>String</code>: "(x, y) z" or "(x, y) z!"
* where <code>x</code> and <code>y</code> is the
* coordinates of the target tile for this mission, and
* <code>z</code> is the value of building the colony. The
* exclamation mark is added if the unit should continue
* searching for a colony site if the targeted site is
* lost.
*/
public String getDebuggingInfo() {
final String targetName = (target != null) ? target.toString()
: "unassigned";
return targetName + " " + colonyValue;
}
// 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 {
if (isValid()) {
toXML(out, getXMLElementTagName());
}
}
/**
* {@inherit-doc}
*/
protected void writeAttributes(XMLStreamWriter out)
throws XMLStreamException {
super.writeAttributes(out);
writeAttribute(out, "target", target);
}
/**
* Reads all the <code>AIObject</code>s and other AI-related information
* from XML data.
*
* @param in The input stream with the XML.
*/
protected void readAttributes(XMLStreamReader in)
throws XMLStreamException {
super.readAttributes(in);
final String targetStr = in.getAttributeValue(null, "target");
target = (targetStr == null) ? null
: (Tile) getGame().getFreeColGameObject(targetStr);
final Unit unit = getUnit();
colonyValue = (unit == null) ? -1
: unit.getOwner().getColonyValue(target);
}
/**
* Returns the tag name of the root element representing this object.
*
* @return "buildColonyMission".
*/
public static String getXMLElementTagName() {
return "buildColonyMission";
}
}