/** * 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.ArrayList; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.logging.Logger; import org.freecolandroid.xml.stream.XMLStreamConstants; 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.Europe; import net.sf.freecol.common.model.FreeColGameObject; import net.sf.freecol.common.model.Goods; import net.sf.freecol.common.model.GoodsContainer; import net.sf.freecol.common.model.GoodsType; import net.sf.freecol.common.model.Locatable; 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.Market; 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.Tile; import net.sf.freecol.common.model.Unit; import net.sf.freecol.common.model.Unit.MoveType; import net.sf.freecol.common.model.Unit.Role; import net.sf.freecol.common.model.UnitType; import net.sf.freecol.common.model.WorkLocation; 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.AIGoods; 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.AIPlayer; import net.sf.freecol.server.ai.AIUnit; import net.sf.freecol.server.ai.EuropeanAIPlayer; import net.sf.freecol.server.ai.GoodsWish; import net.sf.freecol.server.ai.Transportable; import net.sf.freecol.server.ai.Wish; import net.sf.freecol.server.ai.WorkerWish; import org.w3c.dom.Element; /** * Mission for transporting units and goods on a carrier. * * @see net.sf.freecol.common.model.Unit Unit */ public class TransportMission extends Mission { private static final Logger logger = Logger.getLogger(TransportMission.class.getName()); private static final String ELEMENT_TRANSPORTABLE = "transportable"; private static final int MINIMUM_GOLD_TO_STAY_IN_EUROPE = 600; private final List<Transportable> transportList = new ArrayList<Transportable>(); class Destination { private boolean atDestination; private boolean moveToEurope; private PathNode path; /* * Returns an "Already at destination" */ public Destination() { this.atDestination = true; this.moveToEurope = false; this.path = null; } public Destination(boolean moveToEurope, PathNode path) { this.atDestination = false; this.moveToEurope = moveToEurope; this.path = path; } public boolean moveToEurope() { return moveToEurope; } public PathNode getPath() { return path; } public boolean isAtDestination() { return atDestination; } } /** * 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. */ public TransportMission(AIMain aiMain, AIUnit aiUnit) { super(aiMain, aiUnit); if (!getUnit().isCarrier()) { logger.warning("Only carriers can transport unit/goods."); throw new IllegalArgumentException("Only carriers can transport unit/goods."); } } /** * Loads a <code>TransportMission</code> from the given element. * * @param aiMain The main AI-object. * @param element An <code>Element</code> containing an XML-representation * of this object. */ public TransportMission(AIMain aiMain, Element element) { super(aiMain); readFromXMLElement(element); } /** * Creates a new <code>TransportMission</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 TransportMission(AIMain aiMain, XMLStreamReader in) throws XMLStreamException { super(aiMain); readFromXML(in); } /** * Adds every <code>Goods</code> and <code>Unit</code> onboard the * carrier to the transport list. * * @see Goods * @see Unit */ private void updateTransportList() { Unit carrier = getUnit(); Player owner = carrier.getOwner(); // Try to add units that happen to be on board. for (Unit u : carrier.getUnitList()) { AIUnit aiUnit = getAIMain().getAIUnit(u); if (aiUnit == null) { logger.warning("Could not find AI unit for: " + u); continue; } if (aiUnit.getTransportDestination() != null) { addToTransportList(aiUnit); } else if (carrier.isInEurope() || (carrier.getTile() != null && carrier.getTile().isLand())) { // Unit has no destination, drop it off if on land. unitLeavesShip(aiUnit); } } // Remove items that should no longer be transported: // - next step is a null location // - next step is disposed // - next step is a captured settlement List<Transportable> ts = new ArrayList<Transportable>(); for (Transportable t : new ArrayList<Transportable>(transportList)) { if (ts.contains(t) || isCarrying(t)) { Location dst = t.getTransportDestination(); if (dst == null || ((FreeColGameObject)dst).isDisposed() || ((dst instanceof Settlement) && ((Settlement)dst).getOwner() != null && !owner.owns((Settlement)dst))) { removeFromTransportList(t); } } else { Location src = t.getTransportSource(); if (src == null || ((FreeColGameObject)src).isDisposed() || ((src instanceof Settlement) && ((Settlement)src).getOwner() != null && !owner.owns((Settlement)src))) { removeFromTransportList(t); } } ts.add(t); } } /** * Checks if the carrier using this mission is carrying the given * <code>Transportable</code>. * * @param t The <code>Transportable</code>. * @return <code>true</code> if the given <code>Transportable</code> is * {@link Unit#getLocation located} in the carrier. */ private boolean isCarrying(Transportable t) { // TODO: Proper code for checking if the goods is onboard the carrier. return t != null && t.getTransportLocatable() != null && t.getTransportLocatable().getLocation() == getUnit(); } /** * Disposes this <code>Mission</code>. */ public void dispose() { // a new list must be created as the first one may be changed //elsewhere in between loop calls List<Transportable> cargoList = new ArrayList<Transportable>(); List<Transportable> scheduledCargoList = new ArrayList<Transportable>(); Iterator<Transportable> ti = transportList.iterator(); while (ti.hasNext()) { Transportable t = ti.next(); // the cargo is on board, add to list to be disposed of if (isCarrying(t)) { cargoList.add(t); } else { // the cargo was scheduled to be transported // cancel order scheduledCargoList.add(t); } } for (Transportable t : cargoList) ((AIObject) t).dispose(); for (Transportable t : scheduledCargoList) t.setTransport(null); super.dispose(); } /** * Checks if the given <code>Transportable</code> is on the transport * list. * * @param newTransportable The <code>Transportable</code> to be checked * @return <code>true</code> if the given <code>Transportable</code> was * on the transport list, and <code>false</code> otherwise. */ public boolean isOnTransportList(Transportable newTransportable) { for (int i = 0; i < transportList.size(); i++) { if (transportList.get(i) == newTransportable) { return true; } } return false; } /** * Removes the given <code>Transportable</code> from the transport list. * This method calls {@link Transportable#setTransport(AIUnit)}. * * @param transportable The <code>Transportable</code>. */ public void removeFromTransportList(Transportable transportable) { transportList.remove(transportable); if (transportable.getTransport() == getAIUnit()) { transportable.setTransport(null); } } /** * Adds the given <code>Transportable</code> to the transport list. The * method returns immediately if the {@link Transportable} has already be * added. * * <br> * <br> * * Both the source and destination {@link Location} for the * <code>Transportable</code> is entered into the transport list if the * <code>Transportable</code> is not already loaded onto the transport. If * the <code>Transportable</code> is onboard the transport, then only the * destination is put on the transport list. * * @param newTransportable The <code>Transportable</code>. */ public void addToTransportList(Transportable newTransportable) { Unit carrier = getUnit(); if (newTransportable.getTransportLocatable() instanceof Unit && ((Unit) newTransportable.getTransportLocatable()).isCarrier()) { throw new IllegalArgumentException("Can not add a carrier to a transport list."); } Location newSource = newTransportable.getTransportSource(); Location newDestination = newTransportable.getTransportDestination(); if (newDestination == null) { logger.warning("No destination for: " + newTransportable.toString()); return; } if (newSource == null && !isCarrying(newTransportable)) { logger.warning("No source for: " + newTransportable.toString()); return; } if (isOnTransportList(newTransportable)) return; int bestSourceIndex = -1; if (!isCarrying(newTransportable)) { int distToSource; if (carrier.getLocation().getTile() == newSource.getTile()) { distToSource = 0; } else { Tile t = (carrier.getTile() != null) ? carrier.getTile() : carrier.getFullEntryLocation(); distToSource = getDistanceTo(newTransportable, t, true); // Sanitation // Carrier cant reach source if(distToSource == Map.COST_INFINITY){ return; } } bestSourceIndex = 0; int bestSourceDistance = distToSource; for (int i = 1; i < transportList.size() && bestSourceDistance > 0; i++) { Transportable t1 = transportList.get(i - 1); if (t1.getTransportSource() != null && t1.getTransportSource().getTile() == newSource.getTile() || t1.getTransportDestination() != null && t1.getTransportDestination().getTile() == newSource.getTile()) { bestSourceIndex = i; bestSourceDistance = 0; } } for (int i = 1; i < transportList.size() && bestSourceDistance > 0; i++) { Transportable t1 = transportList.get(i - 1); if (isCarrying(t1)){ int distToDestination = getDistanceTo(newTransportable, t1.getTransportDestination(), true); if(distToDestination == Map.COST_INFINITY){ continue; } if(distToDestination <= bestSourceDistance) { bestSourceIndex = i; bestSourceDistance = distToDestination; } } else{ distToSource = getDistanceTo(newTransportable, t1.getTransportSource(), true); if(distToSource == Map.COST_INFINITY){ continue; } if (distToSource <= bestSourceDistance) { bestSourceIndex = i; bestSourceDistance = distToSource; } } } transportList.add(bestSourceIndex, newTransportable); } int bestDestinationIndex = bestSourceIndex + 1; int bestDestinationDistance = Integer.MAX_VALUE; if (bestSourceIndex == -1) { bestDestinationIndex = 0; if (carrier.getTile() == newSource.getTile()) { bestDestinationDistance = 0; } else { int distToCarrier = getDistanceTo(newTransportable, carrier.getTile(), false); if(distToCarrier != Map.COST_INFINITY){ bestDestinationDistance = distToCarrier; } } } for (int i = Math.max(bestSourceIndex, 1); i < transportList.size() && bestDestinationDistance > 0; i++) { Transportable t1 = transportList.get(i - 1); if ((t1.getTransportSource() != null && t1.getTransportSource().getTile() == newDestination.getTile()) || (t1.getTransportDestination() != null && t1.getTransportDestination().getTile() == newDestination.getTile())) { bestDestinationIndex = i; bestDestinationDistance = 0; } } for (int i = Math.max(bestSourceIndex, 1); i < transportList.size() && bestDestinationDistance > 0; i++) { Transportable t1 = transportList.get(i - 1); if (isCarrying(t1)){ int distToDestination = getDistanceTo(newTransportable, t1.getTransportDestination(), false); if(distToDestination == Map.COST_INFINITY){ continue; } if(distToDestination <= bestDestinationDistance) { bestDestinationIndex = i; bestDestinationDistance = distToDestination; } } else{ int distToSource = getDistanceTo(newTransportable, t1.getTransportSource(), false); if(distToSource == Map.COST_INFINITY){ continue; } if(distToSource <= bestDestinationDistance) { bestDestinationIndex = i; bestDestinationDistance = distToSource; } } } transportList.add(bestDestinationIndex, newTransportable); if (newTransportable.getTransport() != getAIUnit()) { newTransportable.setTransport(getAIUnit()); } } /** * Gets the distance to the given <code>Transportable</code>. * * @param start The <code>Location</code> to check the distance from. * <code>Europe</code> is used instead of this location if * <code>start.getTile() == null</code>. * @param source Sets wether the <code>Transportable</code>'s * {@link Transportable#getTransportSource source} or * {@link Transportable#getTransportDestination destination} * should be used. * @return The distance from the given <code>Location</code> to the source * or destination of the given <code>Transportable</code>. */ private int getDistanceTo(Transportable t, Location start, boolean source) { // TODO: This is too expensive - find another method: PathNode path = getTransportPath(t, start, source); return (path == null) ? Map.COST_INFINITY : path.getTotalTurns(); } private boolean canAttackEnemyShips() { final Unit carrier = getUnit(); return carrier.getTile() != null && carrier.isNaval() && carrier.isOffensiveUnit(); } private boolean hasCargo() { final Unit carrier = getUnit(); return (carrier.getGoodsCount() + carrier.getUnitCount()) > 0; } /** * Attack blocking ships. * * @return True if this ship is still capable of its mission. */ private boolean attackIfEnemyShipIsBlocking(Connection connection, Direction direction) { final Unit carrier = getUnit(); if (canAttackEnemyShips() && carrier.getMoveType(direction) == MoveType.ATTACK_UNIT) { final Tile newTile = carrier.getTile().getNeighbourOrNull(direction); final Unit defender = newTile.getDefendingUnit(carrier); if (canAttackPlayer(defender.getOwner())) { AIMessage.askAttack(getAIUnit(), direction); } } return isValid(); } /** * Attack suitable enemy ships. * * @return True if this ship is still capable of its mission. */ private boolean attackEnemyShips(Connection connection) { if (!canAttackEnemyShips()) { return true; } final Unit carrier = getUnit(); if (hasCargo()) { // Do not search for a target if we have cargo onboard. return true; } final PathNode pathToTarget = findNavalTarget(0); if (pathToTarget != null) { final Direction direction = moveTowards(pathToTarget); if (direction != null && carrier.getMoveType(direction) == MoveType.ATTACK_UNIT) { AIMessage.askAttack(getAIUnit(), direction); } } return isValid(); } private boolean canAttackPlayer(Player target) { return getUnit().getOwner().atWarWith(target) || getUnit().hasAbility(Ability.PIRACY); } /** * 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 findNavalTarget(final int maxTurns) { if (!getUnit().isOffensiveUnit()) { throw new IllegalStateException("A target can only be found for offensive units. You tried with: " + getUnit().toString()); } if (!getUnit().isNaval()) { throw new IllegalStateException("A target can only be found for naval units. You tried with: " + getUnit().toString()); } final GoalDecider gd = new GoalDecider() { private PathNode bestTarget = null; private int bestValue = 0; public PathNode getGoal() { return bestTarget; } public boolean hasSubGoals() { return true; } public boolean check(final Unit unit, final PathNode pathNode) { final Tile newTile = pathNode.getTile(); final Unit defender = newTile.getDefendingUnit(unit); if (newTile.isLand() || defender == null || defender.getOwner() == unit.getOwner()) { return false; } if (!canAttackPlayer(defender.getOwner())) { return false; } final int value = 1 + defender.getUnitCount() + defender.getGoodsCount(); if (value > bestValue) { bestTarget = pathNode; bestValue = value; } return true; } }; return getUnit().search(getUnit().getTile(), gd, CostDeciders.avoidSettlements(), maxTurns, null); } /** * Performs the mission. * * @param connection The <code>Connection</code> to the server. */ public void doMission(Connection connection) { logger.finest("Doing transport mission for unit " + getUnit() + "(" + getUnit().getId() + ")"); if (transportList == null || transportList.size() <= 0) { updateTransportList(); } Unit carrier = getUnit(); if (carrier.getMovesLeft() == 0) return; if (carrier.isAtSea()) return; // Going to/from Europe, do nothing if (carrier.isInEurope()) { // Actually in Europe inEurope(connection); return; } if (!attackEnemyShips(connection)) return; restockCargoAtDestination(connection); if (!attackEnemyShips(connection)) return; boolean transportListChanged = false; boolean moreWork = true; for (int i = 0; i < transportList.size() && moreWork || i == 0; i++) { if (carrier.getMovesLeft() == 0) return; moreWork = false; if (transportListChanged) { i = 0; transportListChanged = false; } // Special case, already on a tile which gives direct // access to Europe path will be null. Destination destination = getNextDestination(); boolean canMoveToEurope = destination != null && destination.moveToEurope() && carrier.canMoveToEurope(); if (destination == null || (destination.getPath() == null && !canMoveToEurope)) { String carrierLoc = ""; if (carrier.getLocation() instanceof Europe) { carrierLoc = "Europe"; } else { Tile carrierTile = carrier.getTile(); carrierLoc = carrierTile.toString(); if (carrierTile.getColony() != null) { carrierLoc += " (" + carrierTile.getColony().getName() + ")"; } } logger.info("Could not get a next move for unit " + carrier + "(" + carrier.getId() + "), staying put at " + carrierLoc); //carrier.setMovesLeft(0); return; } if (destination.isAtDestination()) { transportListChanged = restockCargoAtDestination(connection); continue; } // Already on a tile which gives direct access to Europe, // just make the move if (canMoveToEurope) { moveUnitToEurope(); return; } // Move towards the next target: PathNode path = destination.getPath(); boolean moveToEurope = destination.moveToEurope(); Direction r = moveTowards(path); if (r != null && carrier.getMoveType(r).isProgress()) { if (carrier.getMoveType(r) == MoveType.MOVE_HIGH_SEAS && moveToEurope) { moveUnitToEurope(); } else { if (!moveButDontAttack(r)) return; } if (!(carrier.getLocation() instanceof Europe)) { moreWork = true; } } if (r != null) { if (!attackIfEnemyShipIsBlocking(connection, r)) return; } transportListChanged = restockCargoAtDestination(connection); if (!attackEnemyShips(connection)) return; } } /** * Works out the next destination the carrier should go to to * make progress with its transport list. * * @return A new <code>Destination</code>, which may be null if none * is available. */ public Destination getNextDestination() { final Unit carrier = getUnit(); if (transportList.isEmpty() && !hasCargo()) { logger.finest("Next destination for " + carrier + ": default"); return getDefaultDestination(); } // Cache unavailable destinations to avoid unnecessary path finding. List<Location> unavailable = new ArrayList<Location>(); for (Transportable t : transportList) { Location dst = (isCarrying(t)) ? t.getTransportDestination() : t.getTransportLocatable().getLocation(); // Check if we already found this destination to be inaccessible. if (dst == null || unavailable.contains(dst)) continue; PathNode path; if (dst.getTile() == null) { if (dst instanceof Europe && (path = carrier.findPathToEurope()) != null) { logger.finest("Next destination for " + carrier + ": " + dst + " (" + ((isCarrying(t)) ? "transport" : "collect") + " " + t + ")"); return new Destination(true, path); } } else { if (dst.getTile() == carrier.getTile()) { logger.finest("Next destination for " + carrier + ": already at " + dst + " (for " + t + ")"); return new Destination(); // Already at dst! } if ((path = getTransportPath(t)) != null) { logger.finest("Next destination for " + carrier + ": " + dst + " (" + ((isCarrying(t)) ? "transport" : "collect") + " " + t + ")"); return new Destination(false, path); } } unavailable.add(dst); } logger.finest("Next destination for " + carrier + ": none found"); return null; } /** * Gets the current default destination for the unit of this mission. * * @return The default <code>Destination</code> for the unit. */ Destination getDefaultDestination() { Unit unit = getUnit(); PathNode path = null; // If in Europe, stay in Europe if (unit.getLocation() instanceof Europe) { return new Destination(); } // Otherwise should be on the map if (unit.getTile() == null) { throw new IllegalStateException("Unit not on the map: " + unit.getId()); } // Already at a settlement if (unit.getSettlement() != null) { return new Destination(); } // Try nearest colony if ((path = findNearestOtherSettlement(unit)) != null) { return new Destination(false, path); } // Try Europe if (unit.isNaval() && unit.getOwner().canMoveToEurope()) { if (unit.canMoveToEurope()) { return new Destination(true, null); } if ((path = unit.findPathToEurope()) != null) { return new Destination(true, path); } } // Can fail intermittantly. For example: up river and blocked in. logger.warning("Could not get default destination for " + unit); return null; } /** * Buys cargo (units and goods) when the carrier is in <code>Europe</code>. * * <br> * <br> * * <b>Warning:</b> This method can only be called when the carrier is * located in {@link Europe}. * * @param connection The <code>Connection</code> to the server. */ private void buyCargo(Connection connection) { EuropeanAIPlayer aiPlayer = (EuropeanAIPlayer) getAIMain().getAIPlayer(getUnit().getOwner()); if (!getUnit().isInEurope()) { throw new IllegalStateException("Carrier not in Europe"); } /* * Quick fix for forcing the AI to build more colonies. This fix should * be removed after a proper implementation has been created. */ if (aiPlayer.hasFewColonies()) { // since we are in Europe, use the carrier entry point to // search for a good settlement spot. Unit carrier = getUnit(); Tile colonyTile = BuildColonyMission.findTargetTile(getAIUnit(), false); int space = getAvailableSpace(); while (colonyTile != null && space > 0) { AIUnit newUnit = getCheapestUnitInEurope(connection); if (newUnit == null) return; if (newUnit.getUnit().isColonist() && !newUnit.getUnit().isArmed() && !newUnit.getUnit().isMounted() && newUnit.getUnit().getRole() != Role.PIONEER) { // send the colonist to build the new colony newUnit.setMission(new BuildColonyMission(getAIMain(), newUnit, colonyTile)); } addToTransportList(newUnit); space--; } } /* * Add colonies containing wishes with the same destination as an item * in the transport list to the "aiColonies"-list: */ List<AIColony> aiColonies = new ArrayList<AIColony>(); for (int i = 0; i < transportList.size(); i++) { Transportable t = transportList.get(i); if (t.getTransportDestination() != null && t.getTransportDestination().getTile() != null && t.getTransportDestination().getTile().getColony() != null && t.getTransportDestination().getTile().getColony().getOwner() == getUnit().getOwner()) { AIColony ac = getAIMain().getAIColony(t.getTransportDestination().getTile().getColony()); aiColonies.add(ac); } } /* * Add the colony containing the wish with the highest value to the * "aiColonies"-list: */ EuropeanAIPlayer player = (EuropeanAIPlayer) getAIMain().getAIPlayer(getUnit().getOwner()); for (Wish w : player.getWishes()) { if (w.getTransportable() != null) continue; if (w instanceof WorkerWish && w.getDestination() instanceof Colony) { WorkerWish ww = (WorkerWish) w; Colony c = (Colony) ww.getDestination(); AIColony ac = getAIMain().getAIColony(c); if (!aiColonies.contains(ac)) { aiColonies.add(ac); } } else if (w instanceof GoodsWish && w.getDestination() instanceof Colony) { GoodsWish gw = (GoodsWish) w; Colony c = (Colony) gw.getDestination(); AIColony ac = getAIMain().getAIColony(c); if (!aiColonies.contains(ac)) { aiColonies.add(ac); } } else { logger.warning("Unknown type of wish: " + w); } } for (int i = 0; i < aiColonies.size(); i++) { AIColony ac = aiColonies.get(i); // Assuming that all colonists which can be bought in Europe take // the same space: TODO: fix this int space = getAvailableSpace(getUnit().getType(), getUnit().getOwner().getEurope(), ac.getColony()); for (Wish w : ac.getWishes()) { if (space <= 0) break; if (w.getTransportable() != null) continue; if (w instanceof WorkerWish) { WorkerWish ww = (WorkerWish) w; AIUnit newUnit = getUnitInEurope(connection, ww.getUnitType()); if (newUnit != null) { newUnit.setMission(new WishRealizationMission(getAIMain(), newUnit, ww)); ww.setTransportable(newUnit); addToTransportList(newUnit); space--; } } else if (w instanceof GoodsWish) { GoodsWish gw = (GoodsWish) w; AIGoods ag = buyGoodsInEurope(connection, gw.getGoodsType(), GoodsContainer.CARGO_SIZE, gw.getDestination()); if (ag != null) { gw.setTransportable(ag); addToTransportList(ag); space--; } } else { logger.warning("Unknown type of wish: " + w); } } } // Fill the transport with cheap colonists: int space = getAvailableSpace(); while (space > 0) { AIUnit newUnit = getCheapestUnitInEurope(connection); if (newUnit != null) { addToTransportList(newUnit); space--; } else { break; } } } /** * Buys the given cargo. * * <br> * <br> * * <b>Warning:</b> This method can only be called when the carrier is * located in {@link Europe}. * * @param connection The <code>Connection</code> to use when communicating * with the server. * @param type The type of goods to buy. * @param amount The amount of goods to buy. * @param destination The <code>Location</code> to which the goods should * be transported. * @return The goods. */ public AIGoods buyGoodsInEurope(Connection connection, GoodsType type, int amount, Location destination) { AIPlayer aiPlayer = getAIMain().getAIPlayer(getUnit().getOwner()); Player player = aiPlayer.getPlayer(); Market market = player.getMarket(); if (!player.checkGold(market.getBidPrice(type, amount))) return null; boolean success = AIMessage.askBuyGoods(getAIUnit(), type, amount); return (success) ? new AIGoods(getAIMain(), getUnit(), type, amount, destination) : null; } /** * Returns the given type of <code>Unit</code>. * * <br> * <br> * * <b>Warning:</b> This method can only be called when the carrier is * located in {@link Europe}. * * <br> * <br> * * This sequence is used when trying to get the unit: <br> * <br> * <ol> * <li>Getting the unit from the docks. * <li>Recruiting the unit. * <li>Training the unit. * </ol> * * @param connection The <code>Connection</code> to the server. * @param unitType The type of {@link Unit} to be found/recruited/trained. * @return The <code>AIUnit</code>. */ private AIUnit getUnitInEurope(Connection connection, UnitType unitType) { EuropeanAIPlayer aiPlayer = (EuropeanAIPlayer) getAIMain().getAIPlayer(getUnit().getOwner()); Player player = aiPlayer.getPlayer(); Europe europe = player.getEurope(); if (!getUnit().isInEurope()) { throw new IllegalStateException("Carrier not in Europe"); } // Check if the given type of unit appear on the docks: Iterator<Unit> ui = europe.getUnitIterator(); while (ui.hasNext()) { Unit u = ui.next(); if (unitType == null || unitType == u.getType()) { return getAIMain().getAIUnit(u); } } int price = -1; if (unitType.hasPrice() && europe.getUnitPrice(unitType) >= 0) { price = europe.getUnitPrice(unitType); } // Try recruiting the unit, unless it would be cheaper to train. final String selectAbility = "model.ability.selectRecruit"; if (player.checkGold(player.getRecruitPrice()) && price > player.getRecruitPrice() && player.hasAbility(selectAbility)) { for (int i = 0; i < Europe.RECRUIT_COUNT; i++) { if (europe.getRecruitable(i) == unitType) { return aiPlayer.recruitAIUnitInEurope(i); } } } // Try training the unit: if (price > 0 && player.checkGold(price)) { return aiPlayer.trainAIUnitInEurope(unitType); } return null; } /** * Returns the cheapest unit which can be bought in <code>Europe</code>. * * @param connection The connection to use when communicating with the * server. * @return The <code>AIUnit</code>. */ private AIUnit getCheapestUnitInEurope(Connection connection) { EuropeanAIPlayer aiPlayer = (EuropeanAIPlayer) getAIMain().getAIPlayer(getUnit().getOwner()); Player player = aiPlayer.getPlayer(); Europe europe = player.getEurope(); if (!getUnit().isInEurope()) { throw new IllegalStateException("Carrier not in Europe"); } if (!player.canRecruitUnits()) { return null; } // Check if there are any units on the docks: Iterator<Unit> ui = europe.getUnitIterator(); while (ui.hasNext()) { Unit u = ui.next(); if (!u.isCarrier() && getAIMain().getAIUnit(u).getTransport() == null) { return getAIMain().getAIUnit(u); } } int priceTrained = 0; UnitType cheapestTrained = null; List<UnitType> unitTypes = getSpecification().getUnitTypesTrainedInEurope(); for (UnitType unitType : unitTypes) { int price = europe.getUnitPrice(unitType); if (cheapestTrained == null || price < priceTrained) { cheapestTrained = unitType; priceTrained = price; } } // Recruit a random unit. if (player.checkGold(player.getRecruitPrice()) && cheapestTrained != null && player.getRecruitPrice() < priceTrained) { // TODO: Take the best unit (Seasoned scout, pioneer, soldier etc) return aiPlayer.recruitAIUnitInEurope(-1); } // Try training the unit: if (cheapestTrained != null && player.checkGold(priceTrained)) { return aiPlayer.trainAIUnitInEurope(cheapestTrained); } return null; } /** * Returns the path the carrier should use to get/drop the given * <code>Transportable</code>. * * @param transportable The <code>Transportable</code>. * @return The path. */ public PathNode getTransportPath(Transportable transportable) { return getTransportPath(transportable, getUnit().getTile(), !isCarrying(transportable)); } /** * Returns the path the carrier should use to get/drop the given * <code>Transportable</code>. * * @param transportable The <code>Transportable</code>. * @param start The <code>Tile</code> to search from. If * <code>start == null</code> or * <code>start.getTile() == null</code> then the carrier's * {@link Unit#getEntryLocation entry location} is used instead. * @param collect True if the transportable must be collected. * @return The path. */ private PathNode getTransportPath(Transportable transportable, Location start, boolean collect) { Unit carrier = getUnit(); if (start == null || start.getTile() == null) { start = carrier.getFullEntryLocation(); } Locatable locatable = transportable.getTransportLocatable(); Location destination = (collect) ? locatable.getLocation() : transportable.getTransportDestination(); if (destination == null) return null; if (destination.getTile() == null) { return (destination instanceof Europe) ? findPathToEurope(start.getTile()) : null; } PathNode path; if (locatable instanceof Unit && isCarrying(transportable)) { path = ((Unit)locatable).findPath(start.getTile(), destination.getTile(), carrier); if (path == null || path.getTransportDropNode().previous == null) { path = null; } else { path.getTransportDropNode().previous.next = null; } } else { path = carrier.findPath(start.getTile(), destination.getTile()); } return path; } /** * Returns the available space for the given <code>Transportable</code>. * * @param t The <code>Transportable</code> * @return The space available for <code>Transportable</code>s with the * same source and * {@link Transportable#getTransportDestination destination}. */ public int getAvailableSpace(Transportable t) { UnitType type = (t.getTransportLocatable() instanceof Unit) ? ((Unit)t.getTransportLocatable()).getType() : null; return getAvailableSpace(type, t.getTransportSource(), t.getTransportDestination()); } /** * Returns the available space for the given type of <code>Unit</code> at * the given <code>Location</code>. * * @param unitType The type of {@link Unit} or <code>null</code> for * {@link Goods} * @param source The source for the unit. This is where the unit is * presently located. * @param destination The destination for the unit. * @return The space available */ public int getAvailableSpace(UnitType unitType, Location source, Location destination) { // TODO: Implement this method properly: return Math.max(0, getUnit().getSpaceLeft() - transportList.size()); } /** * Returns the available space for any type of unit going to any type of * location. * * @return The space available */ public int getAvailableSpace() { // TODO: Implement this method properly: return Math.max(0, getUnit().getSpaceLeft() - transportList.size()); } /** * Loads and unloads any <code>Transportable</code>. * * @param connection The <code>Connection</code> to the server. * @return <code>true</code> if something has been loaded/unloaded and * <code>false</code>otherwise. */ private boolean restockCargoAtDestination(Connection connection) { return unloadCargoAtDestination(connection) || loadCargoAtDestination(connection); } /** * Unloads any <code>Transportable</code>s which have reached their * destination. * * @param connection The <code>Connection</code> to the server. * @return <code>true</code> if something has been unloaded and * <code>false</code>otherwise. */ private boolean unloadCargoAtDestination(Connection connection) { Map map = getGame().getMap(); Unit carrier = getUnit(); boolean transportListChanged = false; // Sanitation if (carrier.isAtSea()) return false; // Make a copy for iteration, the main list may change inside the loop for (Transportable t : new ArrayList<Transportable>(transportList)) { if (!isCarrying(t)) continue; // To pickup, ignore if (t instanceof AIUnit) { AIUnit au = (AIUnit) t; Unit u = au.getUnit(); Mission mission = au.getMission(); String reason = null; boolean unload = false; Tile destTile; if (mission == null || !mission.isValid()) { // Get rid of the unit ASAP if it has no mission unload = true; reason = "No valid mission"; } else if (au.getTransportDestination() == null) { // Get rid of the unit ASAP if it has no destination unload = true; reason = "No destination"; } else if (au.getTransportDestination() instanceof Europe) { if (carrier.isInEurope()) { // Unload at destination of Europe unload = true; reason = "Arrived in Europe"; } } else if ((destTile = au.getTransportDestination().getTile()) != null) { PathNode p; if (carrier.getTile() == null) { ;// Get back on the map } else if (destTile == carrier.getTile()) { // Unload at destination tile unload = true; reason = "Arrived at " + destTile; } else if ((p = u.findPath(carrier.getTile(), destTile, carrier)) != null) { final PathNode dropNode = p.getTransportDropNode(); int d; if (dropNode != null && dropNode.getTile() != null && (d = dropNode.getTile().getDistanceTo(carrier.getTile())) != Map.COST_INFINITY && d <= 1) { // Next to the drop node, proceed with mission mission.doMission(connection); reason = "Next to drop node " + dropNode.getTile(); } } else { // Destination has become unreachable unload = true; reason = "No path to destination: " + destTile; } } // If unloading, do not drop transported unit into the sea if (unload && (carrier.getSettlement() != null || carrier.isInEurope())) { unitLeavesShip(au); } // If unload or doMission succeeded, update the transportables if (u.getLocation() != carrier) { removeFromTransportList(au); while (transportList.remove(au)); // Make sure its gone! transportListChanged = true; } if (reason != null) { logger.finest("Unloading(" + reason + "," + unload + "): " + u + " from: " + carrier + " -> " + (u.getLocation() != carrier)); } } else if (t instanceof AIGoods) { AIGoods ag = (AIGoods) t; String locStr = (carrier.isInEurope()) ? "Europe" : (carrier.getLocation().getSettlement() != null) ? carrier.getLocation().getSettlement().getName() : carrier.getLocation().toString(); if (ag.getTransportDestination() == null || (ag.getTransportDestination() != null && ag.getTransportDestination().getTile() == carrier.getLocation().getTile())) { logger.finest(carrier + "(" + carrier.getId() + ") unloading " + ag + " at " + locStr); if (carrier.isInEurope()) { boolean success = sellCargoInEurope(ag.getGoods()); if(success){ removeFromTransportList(ag); ag.dispose(); transportListChanged = true; } } else { boolean success = unloadCargoInColony(ag.getGoods()); if(success){ removeFromTransportList(ag); ag.dispose(); transportListChanged = true; } } } } else { logger.warning("Unknown Transportable."); } // Kick the colony if the unit or goods available changes. Colony colony = carrier.getColony(); if (colony != null) { colony.firePropertyChange(Colony.REARRANGE_WORKERS, true, false); } } return transportListChanged; } /** * Loads any <code>Transportable</code>s being in range of the carrier. * * @param connection The <code>Connection</code> to the server. * @return <code>true</code> if something has been unloaded and * <code>false</code>otherwise. */ private boolean loadCargoAtDestination(Connection connection) { Unit carrier = getUnit(); if (carrier.isAtSea()) return false; // TODO: Add code for rendez-vous. boolean transportListChanged = false; Iterator<Transportable> tli = transportList.iterator(); while (tli.hasNext()) { if (carrier.getSpaceLeft() == 0) break; Transportable t = tli.next(); if (isCarrying(t)) continue; // To deliver, ignore. if (t instanceof AIUnit) { AIUnit au = (AIUnit) t; Unit u = au.getUnit(); if (u.isAtSea()) { continue; } else if (carrier.getTile() == u.getTile()) { if (carrier.getTile() != null || (carrier.isInEurope() && u.isInEurope())) { // Drop the transportable from the transport // list without checking if embark succeeds or // not--- if it succeeds all is well, if it // fails it is likely to fail again for the // same unknown reason so we might as well // give up on it and do something else with // the carrier. Similarly also for the goods // loads below. if (u.getLocation() instanceof WorkLocation && ((WorkLocation)u.getLocation()).getColony().getUnitCount() <= 1) { ; // Do not load sole units in colonies. // TODO: do this better. } else { AIMessage.askEmbark(getAIUnit(), u, null); } tli.remove(); transportListChanged = true; } else { throw new IllegalStateException("Bogus" + " carrier at: " + carrier.getLocation() + " unit at: " + u.getLocation()); } } } else if (t instanceof AIGoods) { AIGoods ag = (AIGoods) t; if (carrier.getTile() == ag.getGoods().getTile()) { if (carrier.getTile() != null) { AIMessage.askLoadCargo(getAIUnit(), ag.getGoods()); tli.remove(); transportListChanged = true; } else if (carrier.isInEurope()) { GoodsType goodsType = ag.getGoods().getType(); int goodsAmount = ag.getGoods().getAmount(); if (AIMessage.askBuyGoods(getAIUnit(), goodsType, goodsAmount)) { ag.setGoods(new Goods(getGame(), carrier, goodsType, goodsAmount)); } tli.remove(); transportListChanged = true; } else { throw new IllegalStateException("Bogus carrier at: " + carrier.getLocation()); } } } else { logger.warning("Unknown Transportable: " + t); } // Kick the colony if the unit or goods available changes. Colony colony = carrier.getColony(); if (colony != null) { colony.firePropertyChange(Colony.REARRANGE_WORKERS, true, false); } } return transportListChanged; } /** * Checks if this mission is valid for the given unit. * * @param aiUnit The unit. * @return <code>true</code> if this mission is valid to perform * and <code>false</code> otherwise. */ public static boolean isValid(AIUnit aiUnit) { return Mission.isValid(aiUnit) && aiUnit.getUnit().isCarrier(); } /** * Checks if this mission is still valid to perform. * * @return <code>true</code> */ public boolean isValid() { if (!super.isValid()) return false; updateTransportList(); return !transportList.isEmpty(); } /** * Returns the destination of a required transport. * * @return <code>null</code> */ public Tile getTransportDestination() { return null; } /** * Returns the priority of getting the unit to the transport destination. * * @return 0. */ public int getTransportPriority() { return 0; } /** * Unit is in Europe, unload cargo on board, buy required goods * and board unit. * * @param connection The <code>Connection</code> to the server. */ private void inEurope(Connection connection){ restockCargoAtDestination(connection); buyCargo(connection); restockCargoAtDestination(connection); // Move back to America: Unit carrier = getUnit(); if (!carrier.getOwner().checkGold(MINIMUM_GOLD_TO_STAY_IN_EUROPE) || transportList.size() > 0) { moveUnitToAmerica(); } } /** * Finds the best path to <code>Europe</code>. * * @param start The starting <code>Tile</code>. * @return The path to the target or <code>null</code> if no target can be * found. * @see Europe */ protected PathNode findPathToEurope(Tile start) { return getUnit().findPathToEurope(start); } /** * Gives the number of naval units assigned with a Transport Mission */ public static int getPlayerNavalTransportMissionCount(AIPlayer aiPlayer, Unit unitExcluded){ Player player = aiPlayer.getPlayer(); int units = 0; for(Unit unit : player.getUnits()){ if(unit == unitExcluded){ continue; } if(!unit.isNaval()){ continue; } AIUnit aiUnit = aiPlayer.getAIMain().getAIUnit(unit); if(aiUnit.getMission() instanceof TransportMission){ units++; } } return units; } /** * 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() { return this.toString(); } /** * 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()); } protected void writeChildren(XMLStreamWriter out) throws XMLStreamException { Iterator<Transportable> tli = transportList.iterator(); while (tli.hasNext()) { Transportable t = tli.next(); out.writeStartElement(ELEMENT_TRANSPORTABLE); out.writeAttribute(ID_ATTRIBUTE, ((AIObject) t).getId()); out.writeEndElement(); } } protected void readChildren(XMLStreamReader in) throws XMLStreamException { transportList.clear(); while (in.nextTag() != XMLStreamConstants.END_ELEMENT) { if (in.getLocalName().equals(ELEMENT_TRANSPORTABLE)) { String tid = in.getAttributeValue(null, ID_ATTRIBUTE); AIObject ao = getAIMain().getAIObject(tid); if (ao == null) { if (tid.startsWith(Unit.getXMLElementTagName())) { ao = new AIUnit(getAIMain(), tid); } else { ao = new AIGoods(getAIMain(), tid); } } if (!(ao instanceof Transportable)) { logger.warning("AIObject not Transportable, ID: " + in.getAttributeValue(null, ID_ATTRIBUTE)); } else { transportList.add((Transportable) ao); } in.nextTag(); } else { logger.warning("Unknown tag."); } } } /** * Creates a <code>String</code> representation of this mission to * be used for debugging purposes. */ @Override public String toString() { StringBuilder sb = new StringBuilder(super.toString()); sb.append("\nTransport list:\n"); List<Transportable> ts = new LinkedList<Transportable>(); for(Transportable t : transportList) { Locatable l = t.getTransportLocatable(); sb.append(l.toString()); sb.append(" ("); Location target; if (ts.contains(t) || isCarrying(t)) { sb.append("to "); target = t.getTransportDestination(); } else { sb.append("from "); target = t.getTransportSource(); } if (target instanceof Europe) { sb.append("Europe"); } else if (target == null) { sb.append("null"); } else { sb.append(target.toString()); } sb.append(")"); sb.append("\n"); ts.add(t); } return sb.toString(); } /** * Returns the tag name of the root element representing this object. * * @return "transportMission". */ public static String getXMLElementTagName() { return "transportMission"; } }