/** * 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.HashMap; 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.Unit.UnitState; 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.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; import org.w3c.dom.Element; /** * Mission for controlling a pioneer. * * @see net.sf.freecol.common.model.Unit.Role#PIONEER */ public class PioneeringMission extends Mission { private static final Logger logger = Logger.getLogger(PioneeringMission.class.getName()); /** * 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; /** The improvement this pioneer is to work on. */ private TileImprovementPlan tileImprovementPlan = null; /** A colony to go to to equip if required. */ private Colony colonyWithTools = 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); if (!hasTools()) colonyWithTools = findColonyWithTools(aiUnit); tileImprovementPlan = findTileImprovementPlan(aiUnit); tileImprovementPlan.setPioneer(aiUnit); logger.finest("AI pioneer starts with plan " + tileImprovementPlan + "/" + tileImprovementPlan.getTarget() + ": " + aiUnit.getUnit()); } /** * Loads a mission from the given element. * * @param aiMain The main AI-object. * @param element An <code>Element</code> containing an * XML-representation of this object. */ public PioneeringMission(AIMain aiMain, Element element) { super(aiMain); readFromXMLElement(element); } /** * 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); } /** * 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) { this.tileImprovementPlan = tip; } /** * Abandons the current plan if any. */ private void abandonTileImprovementPlan() { if (tileImprovementPlan != null) { if (tileImprovementPlan.getPioneer() == getAIUnit()) { tileImprovementPlan.setPioneer(null); } tileImprovementPlan = null; } } /** * Disposes of this pioneering mission. */ public void dispose() { abandonTileImprovementPlan(); super.dispose(); } /** * Does a supplied unit have tools? * * @param unit The pioneer <code>Unit</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()); } /** * Checks if a colony can provide the tools required for a pioneer. * * @param aiUnit The <code>AIUnit</code> that needs tools. * @param colony The <code>Colony</code> to check. * @return True if the colony can provide tools. */ private static boolean checkColonyForTools(AIUnit aiUnit, Colony colony) { return colony != null && !colony.isDisposed() && colony.getOwner() == aiUnit.getUnit().getOwner() && colony.canProvideEquipment(Unit.Role.PIONEER .getRoleEquipment(colony.getSpecification())); } /** * Finds the closest colony within MAX_TURNS that can equip the unit. * Public for the test suite. * * @param aiUnit The <code>AIUnit</code> to equip. * @return The closest colony that can equip the unit. */ public static Colony findColonyWithTools(final AIUnit aiUnit) { final Unit unit = aiUnit.getUnit(); if (unit == null || unit.isDisposed()) return null; final Tile startTile = getPathStartTile(unit); if (startTile == null) return null; if (checkColonyForTools(aiUnit, startTile.getColony())) { return startTile.getColony(); } final GoalDecider equipDecider = new GoalDecider() { private PathNode best = null; public PathNode getGoal() { return best; } public boolean hasSubGoals() { return false; } public boolean check(Unit u, PathNode path) { Colony colony = path.getTile().getColony(); if (checkColonyForTools(aiUnit, colony)) { best = path; return true; } return false; } }; PathNode path = unit.search(startTile, equipDecider, CostDeciders.avoidIllegal(), MAX_TURNS, (unit.isOnCarrier()) ? ((Unit)unit.getLocation()) : null); return (path == null) ? null : path.getLastNode().getTile().getColony(); } /** * Weeds out a broken or obsolete tile improvement plan. * * @param tip The <code>TileImprovementPlan</code> to test. * @param aiPlayer The <code>AIPlayer</code> that owns the plan. * @return True if the plan survives this check. */ private static boolean validateTileImprovementPlan(TileImprovementPlan tip, EuropeanAIPlayer aiPlayer) { if (tip == null) return false; Tile target = tip.getTarget(); if (target == null) { logger.warning("Removing targetless TileImprovementPlan"); aiPlayer.removeTileImprovementPlan(tip); tip.dispose(); return false; } if (target.hasImprovement(tip.getType())) { logger.finest("Removing obsolete TileImprovementPlan"); aiPlayer.removeTileImprovementPlan(tip); tip.dispose(); return false; } if (tip.getPioneer() != null && (tip.getPioneer().getUnit() == null || tip.getPioneer().getUnit().isDisposed())) { logger.warning("Clearing broken pioneer for TileImprovementPlan"); tip.setPioneer(null); } return true; } /** * Checks that a tile improvement plan is valid. * * @param tip The <code>TileImprovementPlan</code> to check. * @return True if the plan is valid. */ private boolean checkTileImprovementPlan(TileImprovementPlan tip) { return validateTileImprovementPlan(tip, getEuropeanAIPlayer()); } /** * Finds the best tile improvement plan for a supplied AI unit. * Public for the test suite. * * @param aiUnit The <code>AIUnit</code> to find a plan for. * @return The best available tile improvement plan, or null if none found. */ public static TileImprovementPlan findTileImprovementPlan(AIUnit aiUnit) { final Unit unit = aiUnit.getUnit(); if (unit == null || unit.isDisposed()) return null; final Tile startTile = getPathStartTile(unit); if (startTile == null) return null; // Build the TileImprovementPlan map. final HashMap<Tile, TileImprovementPlan> tipMap = new HashMap<Tile, TileImprovementPlan>(); final EuropeanAIPlayer aiPlayer = (EuropeanAIPlayer)aiUnit.getAIMain().getAIPlayer(unit.getOwner()); for (TileImprovementPlan tip : aiPlayer.getTileImprovementPlans()) { if (!validateTileImprovementPlan(tip, aiPlayer)) continue; if (tip.getPioneer() == aiUnit) return tip; if (tip.getPioneer() != null) continue; if (startTile == tip.getTarget()) return tip; TileImprovementPlan other = tipMap.get(tip.getTarget()); if (other == null || other.getValue() < tip.getValue()) { tipMap.put(tip.getTarget(), tip); } } // Find the best TileImprovementPlan. final GoalDecider tipDecider = new GoalDecider() { private PathNode best = null; private int bestValue = Integer.MIN_VALUE; public PathNode getGoal() { return best; } public boolean hasSubGoals() { return false; } public boolean check(Unit u, PathNode path) { TileImprovementPlan tip = tipMap.get(path.getTile()); if (tip != null) { int value = tip.getValue() - 5 * path.getTotalTurns(); if (value > bestValue) { bestValue = value; best = path; return true; } } return false; } }; if (tipMap.get(startTile) != null) return tipMap.get(startTile); PathNode path = (tipMap.isEmpty()) ? null : unit.search(startTile, tipDecider, CostDeciders.avoidIllegal(), MAX_TURNS, (unit.isOnCarrier()) ? ((Unit)unit.getLocation()) : null); return (path == null) ? null : tipMap.get(path.getLastNode().getTile()); } // Fake Transportable interface. /** * Gets the transport destination for units with this mission. * * @return The destination for this <code>Transportable</code>. */ public Location getTransportDestination() { Tile target = (hasTools()) ? ((!checkTileImprovementPlan(tileImprovementPlan)) ? null : tileImprovementPlan.getTarget()) : ((!checkColonyForTools(getAIUnit(), colonyWithTools)) ? null : colonyWithTools.getTile()); return (shouldTakeTransportToTile(target)) ? target : null; } // Mission interface /** * 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().isPerson() && checkTileImprovementPlan(tileImprovementPlan) && (hasTools() || checkColonyForTools(getAIUnit(), colonyWithTools)); } /** * Checks if this mission is valid for the given unit. * * @param aiUnit The <code>AIUnit</code> to check. * @return True if the AI unit can be assigned a PioneeringMission. */ public static boolean isValid(AIUnit aiUnit) { return Mission.isValid(aiUnit) && aiUnit.getUnit().isPerson() && findTileImprovementPlan(aiUnit) != null && (hasTools(aiUnit) || findColonyWithTools(aiUnit) != null); } /** * 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. * * @param connection The <code>Connection</code> to the server. */ public void doMission(Connection connection) { final Unit unit = getUnit(); if (unit.getTile() == null) { logger.finest("AI pioneer waiting to go to" + getTransportDestination() + ": " + unit); return; } if (!hasTools()) { // Try to equip. if (colonyWithTools != null && !checkColonyForTools(getAIUnit(), colonyWithTools)) { colonyWithTools = null; } if (colonyWithTools == null) { // Find a new colony. colonyWithTools = findColonyWithTools(getAIUnit()); } if (colonyWithTools == null) { abandonTileImprovementPlan(); logger.finest("AI pioneer can not find equipment: " + unit); return; } // Go there, equip unit. if (travelToTarget("AI pioneer", colonyWithTools.getTile()) != Unit.MoveType.MOVE) return; getAIUnit().equipForRole(Unit.Role.PIONEER, false); if (!hasTools()) { abandonTileImprovementPlan(); logger.finest("AI pioneer reached " + colonyWithTools.getName() + " but could not equip: " + unit); return; } logger.finest("AI pioneer reached " + colonyWithTools.getName() + " and equips: " + unit); colonyWithTools = null; } // Check the plan still makes sense. final Player player = unit.getOwner(); final EuropeanAIPlayer aiPlayer = getEuropeanAIPlayer(); if (tileImprovementPlan != null && !validateTileImprovementPlan(tileImprovementPlan, aiPlayer)) { tileImprovementPlan = null; } if (tileImprovementPlan == null) { // Find a new plan. AIUnit aiu = getAIUnit(); tileImprovementPlan = findTileImprovementPlan(aiu); if (tileImprovementPlan == null) { logger.finest("AI pioneer could not find an improvement: " + unit); return; } tileImprovementPlan.setPioneer(aiu); } // Go to target and take control of the land before proceeding // to build. Tile target = tileImprovementPlan.getTarget(); if (travelToTarget("AI pioneer", target) != Unit.MoveType.MOVE) return; if (!player.owns(target)) { // 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(target); if (price < 0) { fail = true; } else { if (price > 0 && !player.checkGold(price)) { price = NetworkConstants.STEAL_LAND; } if (!AIMessage.askClaimLand(aiPlayer.getConnection(), target, null, price) || !player.owns(target)) { // Failed to take ownership fail = true; } } if (fail) { aiPlayer.removeTileImprovementPlan(tileImprovementPlan); tileImprovementPlan.dispose(); tileImprovementPlan = null; logger.finest("AI pioneer can not claim land at " + target + ": " + unit); return; } } if (unit.getState() == UnitState.IMPROVING) { unit.setMovesLeft(0); logger.finest("AI pioneer improving " + tileImprovementPlan.getType() + ": " + unit); } else if (unit.checkSetState(UnitState.IMPROVING)) { // Ask to create the TileImprovement if (AIMessage.askChangeWorkImprovementType(getAIUnit(), tileImprovementPlan.getType())) { logger.finest("AI pioneer began improvement " + tileImprovementPlan.getType() + " at target " + target + ": " + unit); } else { aiPlayer.removeTileImprovementPlan(tileImprovementPlan); tileImprovementPlan.dispose(); tileImprovementPlan = null; logger.finest("AI pioneer failed to improve " + target + ": " + unit); } } else { // Probably just out of moves. logger.finest("AI pioneer waiting to improve at " + target.getId() + ": " + unit); } } /** * Gets debugging information about this mission. * This string is a short representation of this * object's state. * * @return The <code>String</code>: * <ul> * <li>"(x, y) P" (for plowing)</li> * <li>"(x, y) R" (for building road)</li> * <li>"(x, y) Getting tools: (x, y)"</li> * </ul> */ public String getDebuggingInfo() { if (hasTools()) { if (tileImprovementPlan == null) return "No target"; final String action = tileImprovementPlan.getType().getNameKey(); return tileImprovementPlan.getTarget().getPosition().toString() + " " + action; } else { if (colonyWithTools == null) return "No target"; return "Getting tools from " + colonyWithTools.getName(); } } // 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()); } /** * {@inherit-doc} */ protected void writeAttributes(XMLStreamWriter out) throws XMLStreamException { super.writeAttributes(out); writeAttribute(out, "tileImprovementPlan", tileImprovementPlan); } /** * {@inherit-doc} */ protected void readAttributes(XMLStreamReader in) throws XMLStreamException { super.readAttributes(in); final String tileImprovementPlanStr = in.getAttributeValue(null, "tileImprovementPlan"); if (tileImprovementPlanStr != null) { tileImprovementPlan = (TileImprovementPlan) getAIMain().getAIObject(tileImprovementPlanStr); if (tileImprovementPlan == null) { tileImprovementPlan = new TileImprovementPlan(getAIMain(), tileImprovementPlanStr); } } else { tileImprovementPlan = null; } } /** * Returns the tag name of the root element representing this object. * * @return "pioneeringMission". */ public static String getXMLElementTagName() { return "pioneeringMission"; } }