/** * 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; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.logging.Logger; import javax.xml.stream.XMLStreamException; import javax.xml.stream.XMLStreamReader; import net.sf.freecol.common.debug.FreeColDebugger; import net.sf.freecol.common.model.Ability; import net.sf.freecol.common.model.Colony; import net.sf.freecol.common.model.ColonyTradeItem; import net.sf.freecol.common.model.DiplomaticTrade; import net.sf.freecol.common.model.Europe; import net.sf.freecol.common.model.FoundingFather; import net.sf.freecol.common.model.FreeColGameObject; import net.sf.freecol.common.model.GoldTradeItem; import net.sf.freecol.common.model.Goods; import net.sf.freecol.common.model.GoodsTradeItem; import net.sf.freecol.common.model.GoodsType; import net.sf.freecol.common.model.IndianSettlement; import net.sf.freecol.common.model.Location; import net.sf.freecol.common.model.Map; import net.sf.freecol.common.model.Market; import net.sf.freecol.common.model.Modifier; import net.sf.freecol.common.model.PathNode; import net.sf.freecol.common.model.Player; import net.sf.freecol.common.model.Player.PlayerType; import net.sf.freecol.common.model.Player.Stance; import net.sf.freecol.common.model.Settlement; import net.sf.freecol.common.model.Specification; import net.sf.freecol.common.model.StanceTradeItem; import net.sf.freecol.common.model.Tension; import net.sf.freecol.common.model.Tile; import net.sf.freecol.common.model.TradeItem; import net.sf.freecol.common.model.Turn; import net.sf.freecol.common.model.Unit; import net.sf.freecol.common.model.Unit.UnitState; import net.sf.freecol.common.model.UnitTradeItem; import net.sf.freecol.common.model.UnitType; import net.sf.freecol.common.model.WorkLocation; import net.sf.freecol.common.networking.NetworkConstants; import net.sf.freecol.common.model.pathfinding.CostDeciders; import net.sf.freecol.common.util.Utils; import net.sf.freecol.server.ai.mission.BuildColonyMission; import net.sf.freecol.server.ai.mission.CashInTreasureTrainMission; import net.sf.freecol.server.ai.mission.DefendSettlementMission; import net.sf.freecol.server.ai.mission.IdleAtSettlementMission; import net.sf.freecol.server.ai.mission.Mission; import net.sf.freecol.server.ai.mission.MissionaryMission; import net.sf.freecol.server.ai.mission.PioneeringMission; import net.sf.freecol.server.ai.mission.PrivateerMission; import net.sf.freecol.server.ai.mission.ScoutingMission; import net.sf.freecol.server.ai.mission.TransportMission; import net.sf.freecol.server.ai.mission.UnitSeekAndDestroyMission; import net.sf.freecol.server.ai.mission.UnitWanderHostileMission; import net.sf.freecol.server.ai.mission.WishRealizationMission; import net.sf.freecol.server.ai.mission.WorkInsideColonyMission; import net.sf.freecol.server.model.ServerPlayer; /** * Objects of this class contains AI-information for a single {@link * Player} and is used for controlling this player. * * The method {@link #startWorking} gets called by the * {@link AIInGameInputHandler} when it is this player's turn. */ public class EuropeanAIPlayer extends AIPlayer { private static final Logger logger = Logger.getLogger(EuropeanAIPlayer.class.getName()); /** * A comparator to sort units by suitability for a BuildColonyMission. * * Favours unequipped freeColonists, and other unskilled over experts. * Also favour units on the map. */ private static final Comparator<AIUnit> builderComparator = new Comparator<AIUnit>() { private int score(AIUnit a) { Unit unit = a.getUnit(); if (BuildColonyMission.invalidReason(a) != null) return -1000; int base = (!unit.getEquipment().isEmpty()) ? 0 : (unit.getSkillLevel() > 0) ? 100 : 500 + 100 * unit.getSkillLevel(); if (unit.getTile() != null) base += 50; return base; } public int compare(AIUnit a1, AIUnit a2) { return score(a2) - score(a1); } }; /** * A comparator to sort units by suitability for a ScoutingMission. * * Favours existing scouts (especially if on the map), then dismounted * experts, then units that can become scouts. * * We do not check if a unit is near to a colony that can provide horses, * as that is likely to be too expensive. TODO: revise */ private static final Comparator<AIUnit> scoutComparator = new Comparator<AIUnit>() { private int score(AIUnit a) { Unit unit = a.getUnit(); if (!unit.isColonist()) { return -1000; } else if (unit.hasAbility("model.ability.scoutIndianSettlement")) { return 900 + ((unit.getTile() != null) ? 100 : 0); } else if (unit.hasAbility("model.ability.expertScout")) { return 600; } int base = (unit.isInEurope()) ? 500 : (unit.getLocation().getColony() != null && unit.getLocation().getColony() .canProvideEquipment(Unit.Role.SCOUT.getRoleEquipment(unit.getSpecification()))) ? 400 : -1000; if (!unit.getEquipment().isEmpty()) { base -= 400; } else if (unit.getSkillLevel() > 0) { base -= 200; } // Do not penalize criminals or servants. return base; } public int compare(AIUnit a1, AIUnit a2) { return score(a2) - score(a1); } }; /** * A comparator to sort units by suitability for a PioneeringMission. * * Favours existing pioneers (especially if on the map), then experts * missing tools, then units that can become pioneers. * * We do not check if a unit is near to a colony that can provide tools, * as that is likely to be too expensive. TODO: revise */ private static final Comparator<AIUnit> pioneerComparator = new Comparator<AIUnit>() { private int score(AIUnit a) { Unit unit = a.getUnit(); if (!unit.isColonist()) { return -1000; } else if (unit.hasAbility("model.ability.improveTerrain")) { return 900 + ((unit.getTile() != null) ? 100 : 0); } else if (unit.hasAbility("model.ability.expertPioneer")) { return 600; } int base = (unit.isInEurope()) ? 500 : (unit.getLocation().getColony() != null && unit.getLocation().getColony() .canProvideEquipment(Unit.Role.PIONEER.getRoleEquipment(unit.getSpecification()))) ? 400 : -1000; if (!unit.getEquipment().isEmpty()) { base -= 400; } else if (unit.getSkillLevel() > 0) { base -= 200; } else { base += unit.getSkillLevel() * 150; } return base; } public int compare(AIUnit a1, AIUnit a2) { return score(a2) - score(a1); } }; /** A cached map of Tile to best TileImprovementPlan. Do not serialize. */ private final java.util.Map<Tile, TileImprovementPlan> tipMap = new HashMap<Tile, TileImprovementPlan>(); /** * Stores temporary information for sessions (trading with another player * etc). */ private final java.util.Map<String, Integer> sessionRegister = new HashMap<String, Integer>(); /** * Creates a new <code>EuropeanAIPlayer</code>. * * @param aiMain The main AI-class. * @param player The player that should be associated with this * <code>AIPlayer</code>. */ public EuropeanAIPlayer(AIMain aiMain, ServerPlayer player) { super(aiMain, player); uninitialized = getPlayer() == null; } /** * Creates a new <code>AIPlayer</code>. * * @param aiMain The main AI-object. * @param in The input stream containing the XML. * @throws XMLStreamException if a problem was encountered during parsing. */ public EuropeanAIPlayer(AIMain aiMain, XMLStreamReader in) throws XMLStreamException { super(aiMain, in); uninitialized = getPlayer() == null; } /** * Weeds out a broken or obsolete tile improvement plan. * * @param tip The <code>TileImprovementPlan</code> to test. * @return True if the plan survives this check. */ public boolean validateTileImprovementPlan(TileImprovementPlan tip) { if (tip == null) return false; Tile target = tip.getTarget(); if (target == null) { logger.warning("Removing targetless TileImprovementPlan"); tip.dispose(); return false; } if (target.hasImprovement(tip.getType())) { logger.finest("Removing obsolete TileImprovementPlan"); 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; } /** * Builds a map of locations to TileImprovementPlans. * Called by startWorking at the start of every turn. * Public for the test suite. */ public void buildTipMap() { tipMap.clear(); for (AIColony aic : getAIColonies()) { for (TileImprovementPlan tip : aic.getTileImprovementPlans()) { if (!validateTileImprovementPlan(tip)) { aic.removeTileImprovementPlan(tip); continue; } if (tip.getPioneer() != null) continue; TileImprovementPlan other = tipMap.get(tip.getTarget()); if (other == null || other.getValue() < tip.getValue()) { tipMap.put(tip.getTarget(), tip); } } } } /** * Gets the best plan for a tile from the tipMap. * * @param tile The <code>Tile</code> to lookup. * @return The best plan for a tile. */ public TileImprovementPlan getBestPlan(Tile tile) { return (tipMap == null) ? null : tipMap.get(tile); } /** * Gets the best plan for a colony from the tipMap. * * @param colony The <code>Colony</code> to check. * @return The tile with the best plan for a colony, or null if none found. */ public Tile getBestPlanTile(Colony colony) { TileImprovementPlan best = null; int bestValue = Integer.MIN_VALUE; for (Tile t : colony.getOwnedTiles()) { TileImprovementPlan tip = tipMap.get(t); if (tip != null && tip.getValue() > bestValue) { bestValue = tip.getValue(); best = tip; } } return (best == null) ? null : best.getTarget(); } /** * Remove a <code>TileImprovementPlan</code> from the relevant colony. */ public void removeTileImprovementPlan(TileImprovementPlan plan) { for (AIColony aic : getAIColonies()) { if (aic.removeTileImprovementPlan(plan)) break; } } /** * Gets the number of units that should build a colony. * * @return The desired number of colony builders for this player. */ private int buildersNeeded() { Player player = getPlayer(); if (!player.canBuildColonies()) return 0; int nColonies = 0, nPorts = 0, nWorkers = 0; for (Settlement settlement : player.getSettlements()) { nColonies++; if (settlement.isConnectedPort()) nPorts++; for (Unit u : settlement.getUnitList()) { if (u.isPerson()) nWorkers++; } } // If would be good to have at least two colonies, and at least // one port. After that, determine the ratio of workers to colonies // (which should be the average colony size), and if that is above // a threshold, send out another colonist. // The threshold probably should be configurable. 2 is too // low IMHO as it makes a lot of brittle colonies, 3 is too // high at least initially as it slows expansion. For now, // arbitrarily choose e. int result = (nColonies == 0 || nPorts == 0) ? 2 : ((nPorts == 1) && nWorkers >= 3) ? 1 : ((double)nWorkers / nColonies > Math.E) ? 1 : 0; return result; } /** * How many pioneers should we have? * * @return The desired number of pioneers for this player. */ public int pioneersNeeded() { return tipMap.size() / 2; } /** * How many scouts should we have? * * Current scheme for European AIs is to use up to three scouts in * the early part of the game, then one. * * @return The desired number of scouts for this player. */ public int scoutsNeeded() { return (getGame().getTurn().getAge() <= 1) ? 3 : 1; } /** * Asks the server to recruit a unit in Europe on behalf of the AIPlayer. * * TODO: Move this to a specialized Handler class (AIEurope?) * TODO: Give protected access? * * @param index The index of the unit to recruit in the recruitables list, * (if not a valid index, recruit a random unit). * @return The new AIUnit created by this action or null on failure. */ public AIUnit recruitAIUnitInEurope(int index) { AIUnit aiUnit = null; Europe europe = getPlayer().getEurope(); int n = europe.getUnitCount(); final String selectAbility = "model.ability.selectRecruit"; int slot = (index >= 0 && index < Europe.RECRUIT_COUNT && getPlayer().hasAbility(selectAbility)) ? (index + 1) : 0; if (AIMessage.askEmigrate(this, slot) && europe.getUnitCount() == n+1) { aiUnit = getAIUnit(europe.getUnitList().get(n)); } return aiUnit; } /** * Helper function for server communication - Ask the server * to train a unit in Europe on behalf of the AIGetPlayer(). * * TODO: Move this to a specialized Handler class (AIEurope?) * TODO: Give protected access? * * @return the new AIUnit created by this action. May be null. */ public AIUnit trainAIUnitInEurope(UnitType unitType) { if (unitType==null) { throw new IllegalArgumentException("Invalid UnitType."); } AIUnit aiUnit = null; Europe europe = getPlayer().getEurope(); int n = europe.getUnitCount(); if (AIMessage.askTrainUnitInEurope(this, unitType) && europe.getUnitCount() == n+1) { aiUnit = getAIUnit(europe.getUnitList().get(n)); } return aiUnit; } /** * Gets the wishes for all this player's colonies, sorted by the * {@link Wish#getValue value}. * * @return A list of wishes. */ public List<Wish> getWishes() { List<Wish> wishes = new ArrayList<Wish>(); for (AIColony aic : getAIColonies()) { wishes.addAll(aic.getWishes()); } Collections.sort(wishes); return wishes; } /* Internal methods ***********************************************************/ /** * Cheats for the AI. Please try to centralize cheats here. * * TODO: Remove when the AI is good enough. */ private void cheat() { logger.finest("Entering method cheat"); Specification spec = getSpecification(); Market market = getPlayer().getMarket(); for (GoodsType goodsType : spec.getGoodsTypeList()) { if (market.getArrears(goodsType) > 0 && Utils.randomInt(logger, "Cheat boycott", getAIRandom(), 5) == 0) { market.setArrears(goodsType, 0); // Just remove one goods party modifier (we can not // currently identify which modifier applies to which // goods type, but that is not worth fixing for the // benefit of `temporary' cheat code). If we do not // do this, AI colonies accumulate heaps of party // modifiers because of the cheat boycott removal. findOne: for (Colony c : getPlayer().getColonies()) { for (Modifier m : c.getModifiers()) { if ("model.modifier.colonyGoodsParty".equals(m.getSource())) { c.removeModifier(m); break findOne; } } } } } // TODO: This seems to buy units the AIPlayer can't possibly // use (see BR#2566180) if (getAIMain().getFreeColServer().isSinglePlayer() && getPlayer().getPlayerType() == PlayerType.COLONIAL) { Europe europe = getPlayer().getEurope(); List<UnitType> unitTypes = spec.getUnitTypeList(); if (!europe.isEmpty() && scoutsNeeded() > 0 && Utils.randomInt(logger, "Cheat equip scout", getAIRandom(), 4) == 1) { for (Unit u : europe.getUnitList()) { if (u.getRole() == Unit.Role.DEFAULT && getAIUnit(u).equipForRole(Unit.Role.SCOUT, true)) { break; } } } if (Utils.randomInt(logger, "Cheat buy unit", getAIRandom(), 10) == 1) { WorkerWish bestWish = null; int bestValue = Integer.MIN_VALUE; for (AIColony aic : getAIColonies()) { for (WorkerWish ww : aic.getWorkerWishes()) { if (ww.getValue() > bestValue) { bestValue = ww.getValue(); bestWish = ww; } } } UnitType unitType; int unitPrice; if (bestWish != null && (unitType = bestWish.getUnitType()) != null && (unitPrice = europe.getUnitPrice(unitType)) >= 0) { // cheat add the necessary amount of money getPlayer().modifyGold(unitPrice); AIUnit aiUnit = trainAIUnitInEurope(unitType); if (aiUnit != null) { Unit unit = aiUnit.getUnit(); if (unit != null && unit.isColonist()) { aiUnit.equipForRole(Unit.Role.DRAGOON, true); } aiUnit.setMission(new WishRealizationMission(getAIMain(), aiUnit, bestWish)); } } } // TODO: better heuristics to determine which ship to buy if (Utils.randomInt(logger, "Cheat buy ship", getAIRandom(), 40) == 21) { int total = 0; ArrayList<UnitType> navalUnits = new ArrayList<UnitType>(); for (UnitType unitType : unitTypes) { if (unitType.hasAbility(Ability.NAVAL_UNIT) && unitType.hasPrice()) { navalUnits.add(unitType); total += europe.getUnitPrice(unitType); } } UnitType unitToPurchase = null; int r = Utils.randomInt(logger, "Cheat which ship", getAIRandom(), total); total = 0; for (UnitType unitType : navalUnits) { total += unitType.getPrice(); if (r < total) { unitToPurchase = unitType; break; } } getPlayer().modifyGold(europe.getUnitPrice(unitToPurchase)); this.trainAIUnitInEurope(unitToPurchase); } } } /** * Calls {@link AIColony#rearrangeWorkers} for every colony this player * owns. */ private void rearrangeWorkersInColonies() { for (AIColony aic : getAIColonies()) aic.rearrangeWorkers(); } /** * Ensures all units have a mission. */ protected void giveNormalMissions() { final AIMain aiMain = getAIMain(); final Player player = getPlayer(); final int turnNumber = getGame().getTurn().getNumber(); int nBuilders = buildersNeeded(); int nPioneers = pioneersNeeded(); int nScouts = scoutsNeeded(); // Create a mapping of unit type to worker wishes. java.util.Map<UnitType, List<Wish>> workerWishes = new HashMap<UnitType, List<Wish>>(); for (UnitType unitType : getSpecification().getUnitTypeList()) { workerWishes.put(unitType, new ArrayList<Wish>()); } for (Wish w : getWishes()) { if (w instanceof WorkerWish && w.getTransportable() == null) { workerWishes.get(((WorkerWish) w).getUnitType()).add(w); } } // For all units, check if it is a candidate for a new // mission. If it is not a candidate remove it from the // aiUnits list (reporting why not). Adjust the // Build/Pioneer/Scout counts according to the existing valid // missions. List<AIUnit> aiUnits = getAIUnits(); List<AIUnit> navalUnits = new ArrayList<AIUnit>(); String report = ""; int allUnits = aiUnits.size(), i = 0; while (i < aiUnits.size()) { final AIUnit aiUnit = aiUnits.get(i); final Unit unit = aiUnit.getUnit(); Mission m = aiUnit.getMission(); String reason = null; if (unit.isUninitialized() || unit.isDisposed()) { reason = "Invalid-" + aiUnit.toString(); } else if (unit.getState() == UnitState.IN_COLONY && unit.getColony().getUnitCount() <= 1) { // The unit has its hand full keeping the colony alive. if (!(aiUnit.getMission() instanceof WorkInsideColonyMission)){ logger.warning(aiUnit + " should WorkInsideColony at " + unit.getColony().getName()); m = new WorkInsideColonyMission(aiMain, aiUnit, aiMain.getAIColony(unit.getColony())); } reason = "Vital-to-" + unit.getSettlement().getName() + "-" + m.toString(); } else if (unit.isInMission()) { reason = "In-Mission-" + aiUnit.toString(); } else if (m != null && m.isValid() && !m.isOneTime()) { if (m instanceof BuildColonyMission) { nBuilders--; } else if (m instanceof PioneeringMission) { nPioneers--; } else if (m instanceof ScoutingMission) { nScouts--; } reason = "Valid-" + m.toString(); } else if (unit.isNaval()) { aiUnits.remove(i); navalUnits.add(aiUnit); continue; } else if (unit.isAtSea()) { // Wait for it to emerge reason = "At-Sea-" + aiUnit.toString(); } if (reason == null) { i++; } else { report += "\n " + reason; aiUnits.remove(i); } } report = Utils.lastPart(getPlayer().getNationID(), ".") + ".giveNormalMissions(turn=" + turnNumber + " all-units=" + allUnits + " free-land-units=" + aiUnits.size() + " free-naval-units=" + navalUnits.size() + " builders=" + nBuilders + " pioneers=" + nPioneers + " nScouts=" + nScouts + ")" + report; // First try to satisfy the demand for missions with a defined quota. if (nBuilders > 0) { Collections.sort(aiUnits, builderComparator); while (!aiUnits.isEmpty()) { AIUnit aiUnit = aiUnits.get(0); Mission m = getBuildColonyMission(aiUnit); if (m == null) break; // Reached the unsuitable units aiUnits.remove(0); aiUnit.setMission(m); report += "\n New-" + m.toString(); if (--nBuilders <= 0) break; } } if (nScouts > 0) { Collections.sort(aiUnits, scoutComparator); while (!aiUnits.isEmpty()) { AIUnit aiUnit = aiUnits.get(0); Mission m = getScoutingMission(aiUnit); if (m == null) break; // Reached the unsuitable units aiUnits.remove(0); aiUnit.setMission(m); report += "\n New-" + m.toString(); if (--nScouts <= 0) break; } } if (nPioneers > 0) { Collections.sort(aiUnits, pioneerComparator); while (!aiUnits.isEmpty()) { AIUnit aiUnit = aiUnits.get(0); Mission m = getPioneeringMission(aiUnit); if (m == null) break; // Reached the unsuitable units aiUnits.remove(0); aiUnit.setMission(m); report += "\n New-" + m.toString(); if (--nPioneers <= 0) break; } } // Process the free naval units. i = 0; while (i < navalUnits.size()) { final AIUnit aiUnit = navalUnits.get(i); Mission m; if ((m = getPrivateerMission(aiUnit)) != null || (m = getTransportMission(aiUnit)) != null || (m = getSeekAndDestroyMission(aiUnit, 8)) != null || (m = getWanderHostileMission(aiUnit)) != null ) { aiUnit.setMission(m); report += "\n New-" + m.toString(); navalUnits.remove(i); } else { i++; } } // Sort the remaining units, putting naval/transport units at // the front so that we can rely on valid transport missions // for them when handling their passengers. Then loop through // them making sure every unit gets an appropriate mission. i = 0; while (i < aiUnits.size()) { final AIUnit aiUnit = aiUnits.get(i); final Unit unit = aiUnit.getUnit(); Mission m; // CashIn missions are obvious if ((m = getCashInTreasureTrainMission(aiUnit)) != null // Try to maintain defence || (unit.isDefensiveUnit() && (m = getDefendSettlementMission(aiUnit, false)) != null) // Favour wish realization for expert units || (unit.isColonist() && unit.getSkillLevel() > 0 && (m = getWishRealizationMission(aiUnit, workerWishes)) != null) // Try nearby offence || (unit.isOffensiveUnit() && (m = getSeekAndDestroyMission(aiUnit, 8)) != null) // Missionary missions are only available to some units || (m = getMissionaryMission(aiUnit)) != null // Try to satisfy any remaining wishes, such as population || ((m = getWishRealizationMission(aiUnit, workerWishes)) != null) // Another try to defend, with relaxed cost decider || (unit.isDefensiveUnit() && (m = getDefendSettlementMission(aiUnit, true)) != null) // Another try to attack, at longer range || (unit.isOffensiveUnit() && (m = getSeekAndDestroyMission(aiUnit, 16)) != null) // Leftover offensive units should go out looking for trouble || (unit.isOffensiveUnit() && (m = getWanderHostileMission(aiUnit)) != null) ) { aiUnit.setMission(m); report += "\n New-" + m.toString(); aiUnits.remove(i); } else { i++; } } for (AIUnit aiUnit : aiUnits) { Mission m; if (aiUnit.getMission() instanceof IdleAtSettlementMission) { m = aiUnit.getMission(); } else { m = new IdleAtSettlementMission(aiMain, aiUnit); aiUnit.setMission(m); } report += "\n UNUSED-" + m + " at " + aiUnit.getUnit().getLocation(); } for (AIUnit aiUnit : navalUnits) { Mission m; if (aiUnit.getMission() instanceof IdleAtSettlementMission) { m = aiUnit.getMission(); } else { m = new IdleAtSettlementMission(aiMain, aiUnit); aiUnit.setMission(m); } report += "\n UNUSED-" + m + " at " + aiUnit.getUnit().getLocation(); } logger.fine(report); } /** * Gets a new BuildColonyMission for a unit. * * @param aiUnit The <code>AIUnit</code> to check. * @return A new mission, or null if impossible. */ private Mission getBuildColonyMission(AIUnit aiUnit) { final Unit unit = aiUnit.getUnit(); Location loc = (unit.getOwner().canBuildColonies() && unit.isColonist() && BuildColonyMission.invalidReason(aiUnit) == null) ? BuildColonyMission.findTarget(aiUnit, unit.isInEurope()) : null; return (loc != null) ? new BuildColonyMission(getAIMain(), aiUnit, loc) : null; } /** * Gets a new CashInTreasureTrainMission for a unit. * * @param aiUnit The <code>AIUnit</code> to check. * @return A new mission, or null if impossible. */ private Mission getCashInTreasureTrainMission(AIUnit aiUnit) { return (CashInTreasureTrainMission.invalidReason(aiUnit) == null) ? new CashInTreasureTrainMission(getAIMain(), aiUnit) : null; } /** * Gets a new DefendSettlementMission for a unit. * * @param aiUnit The <code>AIUnit</code> to check. * @param relaxed Use a relaxed cost decider to choose the target. * @return A new mission, or null if impossible. */ private Mission getDefendSettlementMission(AIUnit aiUnit, boolean relaxed) { if (DefendSettlementMission.invalidReason(aiUnit) != null) return null; final Unit unit = aiUnit.getUnit(); final Location loc = unit.getLocation(); double worstValue = 1000000.0; Colony worstColony = null; for (AIColony aic : getAIColonies()) { Colony colony = aic.getColony(); if (aic.isBadlyDefended()) { if (Map.isSameLocation(colony.getTile(), loc)) { worstColony = colony; break; } double value = colony.getDefenceRatio() * 100.0 / unit.getTurnsToReach(loc, colony.getTile(), unit.getCarrier(), ((relaxed) ? CostDeciders.numberOfTiles() : null)); if (worstValue > value) { worstValue = value; worstColony = colony; } } } return (worstColony == null) ? null : new DefendSettlementMission(getAIMain(), aiUnit, worstColony); } /** * Gets a new MissionaryMission for a unit. * * @param aiUnit The <code>AIUnit</code> to check. * @return A new mission, or null if impossible. */ private Mission getMissionaryMission(AIUnit aiUnit) { return (MissionaryMission.prepare(aiUnit) == null) ? new MissionaryMission(getAIMain(), aiUnit) : null; } /** * Gets a new PioneeringMission for a unit. * * @param aiUnit The <code>AIUnit</code> to check. * @return A new mission, or null if impossible. */ private Mission getPioneeringMission(AIUnit aiUnit) { Location loc; return (PioneeringMission.prepare(aiUnit) == null && (loc = PioneeringMission.findTarget(aiUnit, true)) != null) // TODO: pioneers to make roads between colonies ? new PioneeringMission(getAIMain(), aiUnit, loc) : null; } /** * Gets a new PrivateerMission for a unit. * * @param aiUnit The <code>AIUnit</code> to check. * @return A new mission, or null if impossible. */ private Mission getPrivateerMission(AIUnit aiUnit) { return (PrivateerMission.invalidReason(aiUnit) == null) ? new PrivateerMission(getAIMain(), aiUnit) : null; } /** * Gets a new ScoutingMission for a unit. * * @param aiUnit The <code>AIUnit</code> to check. * @return A new mission, or null if impossible. */ private Mission getScoutingMission(AIUnit aiUnit) { return (ScoutingMission.invalidReason(aiUnit) == null) ? new ScoutingMission(getAIMain(), aiUnit) : null; } /** * Gets a UnitSeekAndDestroyMission for a unit. * * @param aiUnit The <code>AIUnit</code> to check. * * @return A new mission, or null if impossible. */ public Mission getSeekAndDestroyMission(AIUnit aiUnit, int range) { if (UnitSeekAndDestroyMission.invalidReason(aiUnit) != null) return null; Location target = UnitSeekAndDestroyMission.findTarget(aiUnit, range); return (target == null) ? null : new UnitSeekAndDestroyMission(getAIMain(), aiUnit, target); } /** * Gets a new TransportMission for a unit. * * @param aiUnit The <code>AIUnit</code> to check. * @return A new mission, or null if impossible. */ private Mission getTransportMission(AIUnit aiUnit) { return (TransportMission.invalidReason(aiUnit) == null) ? new TransportMission(getAIMain(), aiUnit) : null; } /** * Gets the best worker wish for a unit. * * @param aiUnit The <code>AIUnit</code> to check. * @param workerWishes A map of unit type to wishes. * @return The best worker wish for the unit. */ private WorkerWish getBestWorkerWish(AIUnit aiUnit, java.util.Map<UnitType, List<Wish>> workerWishes) { final Unit unit = aiUnit.getUnit(); List<Wish> wishList = workerWishes.get(unit.getType()); WorkerWish nonTransported = null; WorkerWish transported = null; float bestNonTransportedValue = -1.0f; float bestTransportedValue = -1.0f; for (Wish w : wishList) { WorkerWish ww = (WorkerWish)w; int turns = unit.getTurnsToReach(ww.getDestination()); if (turns == INFINITY) { if (bestTransportedValue < ww.getValue()) { bestTransportedValue = ww.getValue(); transported = ww; } } else { if (bestNonTransportedValue < (float)ww.getValue() / turns) { bestNonTransportedValue = (float)ww.getValue() / turns; nonTransported = ww; } } } return (nonTransported != null) ? nonTransported : (transported != null) ? transported : null; } /** * Gets a new UnitWanderHostileMission for a unit. * * @param aiUnit The <code>AIUnit</code> to check. * @return A new mission, or null if impossible. */ private Mission getWanderHostileMission(AIUnit aiUnit) { return (UnitWanderHostileMission.invalidReason(aiUnit) == null) ? new UnitWanderHostileMission(getAIMain(), aiUnit) : null; } /** * Gets a new WishRealizationMission for a unit. * * @param aiUnit The <code>AIUnit</code> to check. * @param workerWishes A map of unit type to wishes. * @param best Optionally override the <code>WorkerWish</code>. * @return A new mission, or null if impossible. */ private Mission getWishRealizationMission(AIUnit aiUnit, java.util.Map<UnitType, List<Wish>> workerWishes) { WorkerWish best = getBestWorkerWish(aiUnit, workerWishes); if (best == null) return null; final Unit unit = aiUnit.getUnit(); best.setTransportable(aiUnit); List<Wish> wishList = workerWishes.get(unit.getType()); wishList.remove(best); workerWishes.put(unit.getType(), wishList); return new WishRealizationMission(getAIMain(), aiUnit, best); } /** * Brings gifts to nice players with nearby colonies. * * TODO: European players can also bring gifts! However, * this might be folded into a trade mission, since * European gifts are just a special case of trading. */ private void bringGifts() { return; } /** * Demands goods from players with nearby colonies. * * TODO: European players can also demand tribute! */ private void demandTribute() { return; } /** * Calls {@link AIColony#updateAIGoods()} for every colony this * player owns. */ private void createAIGoodsInColonies() { logger.finest("Entering method createAIGoodsInColonies"); for (AIColony aic : getAIColonies()) aic.updateAIGoods(); } /** * Assign transportable units and goods to available carriers * transport lists. */ private void createTransportLists() { if (!getPlayer().isEuropean()) return; List<Transportable> transportables = new ArrayList<Transportable>(); // Collect non-naval units needing transport. for (AIUnit au : getAIUnits()) { if (!au.getUnit().isNaval() && au.getTransport() == null && au.getTransportDestination() != null) { transportables.add(au); } } // Add goods to transport for (AIColony aic : getAIColonies()) { for (AIGoods aig : aic.getAIGoods()) { if (aig.getTransportDestination() != null && aig.getTransport() == null) { transportables.add(aig); } } } // Update the priority. for (Transportable t : transportables) { t.increaseTransportPriority(); } // Order the transportables by priority. Collections.sort(transportables, Transportable.transportableComparator); // Collect current transport missions. ArrayList<TransportMission> availableMissions = new ArrayList<TransportMission>(); for (AIUnit au : getAIUnits()) { if (au.getMission() instanceof TransportMission) { availableMissions.add((TransportMission)au.getMission()); } } // For all transportables, find the best carrier. // // That is the one with space available that is closest. // TODO: this concentrates on packing, but ignores destinations // which is definitely going to be inefficient // TODO: be smarter about removing bestTransport from // availableMissions when it is full. while (!transportables.isEmpty() && !availableMissions.isEmpty()) { Transportable t = transportables.remove(0); Location loc = t.getTransportLocatable().getLocation(); // Leave existing transport arrangements intact. if (loc instanceof Unit) { AIUnit aiCarrier = getAIUnit((Unit)loc); Mission m = aiCarrier.getMission(); if (m instanceof TransportMission) { TransportMission tm = (TransportMission)m; if (tm.isOnTransportList(t) || tm.addToTransportList(t)) { logger.finest("Transport continuing: " + t); continue; } } } TransportMission bestTransport = null; int bestTransportSpace = 0; int bestTransportTurns = Integer.MAX_VALUE; for (TransportMission tm : availableMissions) { int transportSpace = tm.getAvailableSpace(t); if (transportSpace <= 0) continue; if (t instanceof AIUnit) { if (!tm.getUnit().canCarryUnits()) continue; } else if (t instanceof AIGoods) { if (!tm.getUnit().canCarryGoods()) continue; } if (t.getTransportSource() != null && (t.getTransportSource().getTile() == tm.getUnit().getTile())) { if (bestTransportTurns > 0 || (bestTransportTurns == 0 && transportSpace > bestTransportSpace)) { bestTransport = tm; bestTransportSpace = transportSpace; bestTransportTurns = 0; } continue; } PathNode p; int totalTurns = (t.getTransportDestination() != null && t.getTransportDestination().getTile() == tm.getUnit().getTile()) ? 0 : ((p = tm.getTransportPath(t)) == null) ? -1 : p.getTotalTurns(); if (totalTurns <= 0) continue; if (totalTurns < bestTransportTurns || (totalTurns == bestTransportTurns && transportSpace > bestTransportSpace)) { bestTransport = tm; bestTransportSpace = transportSpace; bestTransportTurns = totalTurns; } } if (bestTransport == null) { logger.finest("Transport unavailable: " + t); continue; } else if (bestTransport.addToTransportList(t)) { logger.finest("Transport found for: " + t + " using: " + bestTransport); } else { logger.finest("Transport failed for: " + t + " using: " + bestTransport); } } } // AIPlayer interface /** * Tells this <code>AIPlayer</code> to make decisions. The * <code>AIPlayer</code> is done doing work this turn when this method * returns. */ public void startWorking() { Turn turn = getGame().getTurn(); logger.finest(getClass().getName() + " in " + turn + ": " + Utils.lastPart(getPlayer().getNationID(), ".")); sessionRegister.clear(); clearAIUnits(); cheat(); determineStances(); if (turn.isFirstTurn()) initializeMissions(); buildTipMap(); rearrangeWorkersInColonies(); abortInvalidAndOneTimeMissions(); giveNormalMissions(); bringGifts(); demandTribute(); createAIGoodsInColonies(); createTransportLists(); doMissions(); rearrangeWorkersInColonies(); abortInvalidMissions(); giveNormalMissions(); doMissions(); rearrangeWorkersInColonies(); abortInvalidMissions(); clearAIUnits(); } /** * Simple initialization of AI missions given that we know the starting * conditions. */ private void initializeMissions() { // Full debug setup is very different. if (FreeColDebugger.getDebugLevel() >= FreeColDebugger.DEBUG_FULL) return; AIMain aiMain = getAIMain(); // Give the ship a transport mission. TransportMission tm = null; for (AIUnit aiu : getAIUnits()) { Unit u = aiu.getUnit(); if (u.isNaval() && !aiu.hasMission()) { aiu.setMission(tm = new TransportMission(aiMain, aiu)); } } // Find a colony site, give the land units build colony missions, // and add them to the ship's transport mission. Location target = null; for (AIUnit aiu : getAIUnits()) { Unit u = aiu.getUnit(); if (!u.isNaval() && !aiu.hasMission()) { if (target == null) { target = BuildColonyMission.findTarget(aiu, false); } aiu.setMission(new BuildColonyMission(aiMain, aiu, target)); tm.addToTransportList(aiu); } } } /** * Evaluates a proposed mission type for a unit, specialized for * European players. * * @param aiUnit The <code>AIUnit</code> to perform the mission. * @param path A <code>PathNode</code> to the target of this mission. * @param type The mission type. * @return A score representing the desirability of this mission. */ public int scoreMission(AIUnit aiUnit, PathNode path, Class type) { int value = super.scoreMission(aiUnit, path, type); if (value > 0) { if (type == DefendSettlementMission.class) { // Reduce value in proportion to the number of defenders. Colony colony = (Colony)DefendSettlementMission .extractTarget(aiUnit, path); int defenders = getSettlementDefenders(colony); value -= 25 * defenders; // Reduce value according to the stockade level. if (colony.hasStockade()) { if (defenders > colony.getStockade().getLevel() + 1) { value -= 100 * colony.getStockade().getLevel(); } else { value -= 20 * colony.getStockade().getLevel(); } } } } return value; } /** * Decides whether to accept an Indian demand, or not. * * @param unit The <code>Unit</code> making demands. * @param colony The <code>Colony</code> where demands are being made. * @param goods The <code>Goods</code> demanded. * @param gold The amount of gold demanded. * @return True if this player accepts the demand. */ public boolean indianDemand(Unit unit, Colony colony, Goods goods, int gold) { // TODO: make a better choice, check whether the colony is // well defended return !"conquest".equals(getAIAdvantage()); } public boolean acceptDiplomaticTrade(DiplomaticTrade agreement) { boolean validOffer = true; Stance stance = null; int value = 0; Iterator<TradeItem> itemIterator = agreement.iterator(); while (itemIterator.hasNext()) { TradeItem item = itemIterator.next(); if (item instanceof GoldTradeItem) { int gold = ((GoldTradeItem) item).getGold(); if (item.getSource() == getPlayer()) { value -= gold; } else { value += gold; } } else if (item instanceof StanceTradeItem) { // TODO: evaluate whether we want this stance change stance = ((StanceTradeItem) item).getStance(); switch (stance) { case UNCONTACTED: validOffer = false; //never accept invalid stance change break; case WAR: // always accept war without cost break; case CEASE_FIRE: value -= 500; break; case PEACE: if (!agreement.getSender().hasAbility("model.ability.alwaysOfferedPeace")) { // TODO: introduce some kind of counter in order to avoid // Benjamin Franklin exploit value -= 1000; } break; case ALLIANCE: value -= 2000; break; } } else if (item instanceof ColonyTradeItem) { // TODO: evaluate whether we might wish to give up a colony if (item.getSource() == getPlayer()) { validOffer = false; break; } else { value += 1000; } } else if (item instanceof UnitTradeItem) { // TODO: evaluate whether we might wish to give up a unit if (item.getSource() == getPlayer()) { validOffer = false; break; } else { value += 100; } } else if (item instanceof GoodsTradeItem) { Goods goods = ((GoodsTradeItem) item).getGoods(); if (item.getSource() == getPlayer()) { value -= getPlayer().getMarket().getBidPrice(goods.getType(), goods.getAmount()); } else { value += getPlayer().getMarket().getSalePrice(goods.getType(), goods.getAmount()); } } } if (validOffer) { logger.info("Trade value is " + value + ", accept if >=0"); } else { logger.info("Trade offer is considered invalid!"); } return (value>=0)&&validOffer; } /** * Called after another <code>Player</code> sends a * <code>trade</code> message * * @param goods The <code>Goods</code> to offer. */ public void registerSellGoods(Goods goods) { String goldKey = "tradeGold#" + goods.getType().getId() + "#" + goods.getAmount() + "#" + goods.getLocation().getId(); sessionRegister.put(goldKey, null); } /** * Called when another <code>Player</code> proposes to buy. * * TODO: this obviously applies only to native players. Is there * an European equivalent? * * @param unit The foreign <code>Unit</code> trying to trade. * @param settlement The <code>Settlement</code> this player owns and * which the given <code>Unit</code> is trading. * @param goods The goods the given <code>Unit</code> is trying to sell. * @param gold The suggested price. * @return The price this <code>AIPlayer</code> suggests or * {@link NetworkConstants#NO_TRADE}. */ public int buyProposition(Unit unit, Settlement settlement, Goods goods, int gold) { logger.finest("Entering method buyProposition"); Player buyer = unit.getOwner(); String goldKey = "tradeGold#" + goods.getType().getId() + "#" + goods.getAmount() + "#" + settlement.getId(); String hagglingKey = "tradeHaggling#" + unit.getId(); Integer registered = sessionRegister.get(goldKey); if (registered == null) { int price = ((IndianSettlement) settlement).getPriceToSell(goods) + getPlayer().getTension(buyer).getValue(); Unit missionary = ((IndianSettlement) settlement).getMissionary(buyer); if (missionary != null && getSpecification() .getBoolean("model.option.enhancedMissionaries")) { // 10% bonus for missionary, 20% if expert int bonus = (missionary.hasAbility(Ability.EXPERT_MISSIONARY)) ? 8 : 9; price = (price * bonus) / 10; } sessionRegister.put(goldKey, new Integer(price)); return price; } else { int price = registered.intValue(); if (price < 0 || price == gold) { return price; } else if (gold < (price * 9) / 10) { logger.warning("Cheating attempt: sending a offer too low"); sessionRegister.put(goldKey, new Integer(-1)); return NetworkConstants.NO_TRADE; } else { int haggling = 1; if (sessionRegister.containsKey(hagglingKey)) { haggling = sessionRegister.get(hagglingKey).intValue(); } if (Utils.randomInt(logger, "Buy gold", getAIRandom(), 3 + haggling) <= 3) { sessionRegister.put(goldKey, new Integer(gold)); sessionRegister.put(hagglingKey, new Integer(haggling + 1)); return gold; } else { sessionRegister.put(goldKey, new Integer(-1)); return NetworkConstants.NO_TRADE_HAGGLE; } } } } /** * Called when another <code>Player</code> proposes a sale. * * @param unit The foreign <code>Unit</code> trying to trade. * @param settlement The <code>Settlement</code> this player owns and * which the given <code>Unit</code> if trying to sell goods. * @param goods The goods the given <code>Unit</code> is trying to sell. * @param gold The suggested price. * @return The price this <code>AIPlayer</code> suggests or * {@link NetworkConstants#NO_TRADE}. */ public int sellProposition(Unit unit, Settlement settlement, Goods goods, int gold) { logger.finest("Entering method sellProposition"); Colony colony = (Colony) settlement; Player otherPlayer = unit.getOwner(); // don't pay for more than fits in the warehouse int amount = colony.getWarehouseCapacity() - colony.getGoodsCount(goods.getType()); amount = Math.min(amount, goods.getAmount()); // get a good price Tension.Level tensionLevel = getPlayer().getTension(otherPlayer).getLevel(); int percentage = (9 - tensionLevel.ordinal()) * 10; // what we could get for the goods in Europe (minus taxes) int netProfits = ((100 - getPlayer().getTax()) * getPlayer().getMarket().getSalePrice(goods.getType(), amount)) / 100; int price = (netProfits * percentage) / 100; return price; } /** * Decides whether to accept the monarch's tax raise or not. * * @param tax The new tax rate to be considered. * @return <code>true</code> if the tax raise should be accepted. */ public boolean acceptTax(int tax) { Goods toBeDestroyed = getPlayer().getMostValuableGoods(); if (toBeDestroyed == null) { return false; } GoodsType goodsType = toBeDestroyed.getType(); if (goodsType.isFoodType() || goodsType.isBreedable()) { // we should be able to produce food and horses ourselves // TODO: check whether we already have horses! return false; } else if (goodsType.isMilitaryGoods() || goodsType.isTradeGoods() || goodsType.isBuildingMaterial()) { if (getGame().getTurn().getAge() == 3) { // by this time, we should be able to produce // enough ourselves // TODO: check whether we have an armory, at least return false; } else { return true; } } else { int averageIncome = 0; int numberOfGoods = 0; // TODO: consider the amount of goods produced. If we // depend on shipping huge amounts of cheap goods, we // don't want these goods to be boycotted. List<GoodsType> goodsTypes = getSpecification().getGoodsTypeList(); for (GoodsType type : goodsTypes) { if (type.isStorable()) { averageIncome += getPlayer().getIncomeAfterTaxes(type); numberOfGoods++; } } averageIncome = averageIncome / numberOfGoods; if (getPlayer().getIncomeAfterTaxes(toBeDestroyed.getType()) > averageIncome) { // this is a more valuable type of goods return false; } else { return true; } } } /** * Decides to accept an offer of mercenaries or not. * TODO: make a better choice. * * @return True if the mercenaries are accepted. */ public boolean acceptMercenaries() { return getPlayer().isAtWar() || "conquest".equals(getAIAdvantage()); } /** * Selects the most useful founding father offered. * TODO: improve choice * * @param foundingFathers The founding fathers on offer. * @return The founding father selected. */ public FoundingFather selectFoundingFather(List<FoundingFather> foundingFathers) { int age = getGame().getTurn().getAge(); FoundingFather bestFather = null; int bestWeight = Integer.MIN_VALUE; for (FoundingFather father : foundingFathers) { if (father == null) continue; // For the moment, arbitrarily: always choose the one // offering custom houses. Allowing the AI to build CH // early alleviates the complexity problem of handling all // TransportMissions correctly somewhat. if (father.hasAbility("model.ability.buildCustomHouse")) { bestFather = father; break; } int weight = father.getWeight(age); if (weight > bestWeight) { bestWeight = weight; bestFather = father; } } return bestFather; } }