mirror of https://github.com/FreeCol/freecol.git
3035 lines
116 KiB
Java
3035 lines
116 KiB
Java
/**
|
|
* Copyright (C) 2002-2022 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 static net.sf.freecol.common.model.Constants.INFINITY;
|
|
import static net.sf.freecol.common.model.Constants.UNDEFINED;
|
|
import static net.sf.freecol.common.util.CollectionUtils.any;
|
|
import static net.sf.freecol.common.util.CollectionUtils.appendToMapList;
|
|
import static net.sf.freecol.common.util.CollectionUtils.ascendingIntegerComparator;
|
|
import static net.sf.freecol.common.util.CollectionUtils.cacheDouble;
|
|
import static net.sf.freecol.common.util.CollectionUtils.cachingDoubleComparator;
|
|
import static net.sf.freecol.common.util.CollectionUtils.count;
|
|
import static net.sf.freecol.common.util.CollectionUtils.first;
|
|
import static net.sf.freecol.common.util.CollectionUtils.flatten;
|
|
import static net.sf.freecol.common.util.CollectionUtils.forEachMapEntry;
|
|
import static net.sf.freecol.common.util.CollectionUtils.isNotNull;
|
|
import static net.sf.freecol.common.util.CollectionUtils.map;
|
|
import static net.sf.freecol.common.util.CollectionUtils.mapEntriesByValue;
|
|
import static net.sf.freecol.common.util.CollectionUtils.matchKey;
|
|
import static net.sf.freecol.common.util.CollectionUtils.maximize;
|
|
import static net.sf.freecol.common.util.CollectionUtils.sort;
|
|
import static net.sf.freecol.common.util.CollectionUtils.sum;
|
|
import static net.sf.freecol.common.util.CollectionUtils.transform;
|
|
import static net.sf.freecol.common.util.RandomUtils.getRandomMember;
|
|
import static net.sf.freecol.common.util.RandomUtils.randomInt;
|
|
import static net.sf.freecol.common.util.RandomUtils.randomInts;
|
|
|
|
import java.util.ArrayList;
|
|
import java.util.Collections;
|
|
import java.util.Comparator;
|
|
import java.util.HashMap;
|
|
import java.util.HashSet;
|
|
import java.util.List;
|
|
import java.util.Map.Entry;
|
|
import java.util.Random;
|
|
import java.util.Set;
|
|
import java.util.function.Function;
|
|
import java.util.function.Predicate;
|
|
import java.util.function.ToDoubleFunction;
|
|
import java.util.logging.Level;
|
|
import java.util.logging.Logger;
|
|
import java.util.stream.Collectors;
|
|
|
|
import javax.xml.stream.XMLStreamException;
|
|
|
|
import net.sf.freecol.common.FreeColException;
|
|
import net.sf.freecol.common.i18n.Messages;
|
|
import net.sf.freecol.common.io.FreeColXMLReader;
|
|
import net.sf.freecol.common.model.Ability;
|
|
import net.sf.freecol.common.model.AbstractUnit;
|
|
import net.sf.freecol.common.model.Building;
|
|
import net.sf.freecol.common.model.Colony;
|
|
import net.sf.freecol.common.model.ColonyTradeItem;
|
|
import net.sf.freecol.common.model.Constants.IndianDemandAction;
|
|
import net.sf.freecol.common.model.DiplomaticTrade;
|
|
import net.sf.freecol.common.model.DiplomaticTrade.TradeContext;
|
|
import net.sf.freecol.common.model.DiplomaticTrade.TradeStatus;
|
|
import net.sf.freecol.common.model.Europe;
|
|
import net.sf.freecol.common.model.FoundingFather;
|
|
import net.sf.freecol.common.model.Game;
|
|
import net.sf.freecol.common.model.GoldTradeItem;
|
|
import net.sf.freecol.common.model.Goods;
|
|
import net.sf.freecol.common.model.GoodsType;
|
|
import net.sf.freecol.common.model.HistoryEvent;
|
|
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.NationSummary;
|
|
import net.sf.freecol.common.model.NativeTrade;
|
|
import net.sf.freecol.common.model.NativeTrade.NativeTradeAction;
|
|
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.Role;
|
|
import net.sf.freecol.common.model.Settlement;
|
|
import net.sf.freecol.common.model.Specification;
|
|
import net.sf.freecol.common.model.Stance;
|
|
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.UnitType;
|
|
import net.sf.freecol.common.model.pathfinding.CostDeciders;
|
|
import net.sf.freecol.common.model.pathfinding.GoalDeciders;
|
|
import net.sf.freecol.common.option.GameOptions;
|
|
import net.sf.freecol.common.util.CachingFunction;
|
|
import net.sf.freecol.common.util.LogBuilder;
|
|
import net.sf.freecol.common.util.RandomChoice;
|
|
import net.sf.freecol.server.ai.military.MilitaryCoordinator;
|
|
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 European
|
|
* {@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 MissionAIPlayer {
|
|
|
|
private static final Logger logger = Logger.getLogger(EuropeanAIPlayer.class.getName());
|
|
|
|
/** Predicate to select units that can be equipped. */
|
|
private static final Predicate<Unit> equipPred = u ->
|
|
u.hasDefaultRole() && u.hasAbility(Ability.CAN_BE_EQUIPPED);
|
|
|
|
/** Predicate to select party modifiers. */
|
|
private static final Predicate<Modifier> partyPred
|
|
= matchKey(Specification.COLONY_GOODS_PARTY_SOURCE,
|
|
Modifier::getSource);
|
|
|
|
/** Maximum number of turns to travel to a building site. */
|
|
private static final int buildingRange = 5;
|
|
|
|
/** Maximum number of turns to travel to a cash in location. */
|
|
private static final int cashInRange = 20;
|
|
|
|
/** Maximum number of turns to travel to a missionary target. */
|
|
private static final int missionaryRange = 20;
|
|
|
|
/**
|
|
* 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 pioneeringRange = 10;
|
|
|
|
/**
|
|
* Maximum number of turns to travel to a privateering target.
|
|
* Low number because of large naval moves.
|
|
*/
|
|
private static final int privateerRange = 1;
|
|
|
|
/** Maximum number of turns to travel to a scouting target. */
|
|
private static final int scoutingRange = 20;
|
|
|
|
/** A comparator to sort units by decreasing builder score. */
|
|
private static final Comparator<AIUnit> builderComparator
|
|
= Comparator.comparingInt(AIUnit::getBuilderScore).reversed();
|
|
|
|
/**
|
|
* A comparator to sort units by suitability for a PioneeringMission.
|
|
*
|
|
* We do not check if a unit is near to a colony that can provide tools,
|
|
* as that is likely to be too expensive. FIXME: perhaps we should.
|
|
*/
|
|
public static final Comparator<AIUnit> pioneerComparator
|
|
= Comparator.comparingInt(AIUnit::getPioneerScore).reversed();
|
|
|
|
/**
|
|
* A comparator to sort units by suitability for a ScoutingMission.
|
|
*
|
|
* We do not check if a unit is near to a colony that can provide horses,
|
|
* as that is likely to be too expensive. FIXME: perhaps we should.
|
|
*/
|
|
public static final Comparator<AIUnit> scoutComparator
|
|
= Comparator.comparingInt(AIUnit::getScoutScore).reversed();
|
|
|
|
|
|
// These should be final, but need the spec.
|
|
|
|
/** Cheat chances. */
|
|
private static int liftBoycottCheatPercent;
|
|
private static int equipScoutCheatPercent;
|
|
private static int equipPioneerCheatPercent;
|
|
private static int landUnitCheatPercent;
|
|
private static int offensiveLandUnitCheatPercent;
|
|
private static int offensiveNavalUnitCheatPercent;
|
|
private static int transportNavalUnitCheatPercent;
|
|
/** The pioneer role. */
|
|
private static Role pioneerRole = null;
|
|
/** The scouting role. */
|
|
private static Role scoutRole = null;
|
|
|
|
// Caches/internals. Do not serialize.
|
|
|
|
/**
|
|
* A cached map of Tile to best TileImprovementPlan.
|
|
* Used to choose a tile improvement for a pioneer to work on.
|
|
*/
|
|
private final java.util.Map<Tile, TileImprovementPlan> tipMap
|
|
= new HashMap<>();
|
|
|
|
/**
|
|
* A cached map of destination Location to Wishes awaiting transport.
|
|
*/
|
|
private final java.util.Map<Location, List<Wish>> transportDemand
|
|
= new HashMap<>();
|
|
|
|
/** A cached list of transportables awaiting transport. */
|
|
private final List<TransportableAIObject> transportSupply
|
|
= new ArrayList<>();
|
|
|
|
/**
|
|
* A mapping of goods type to the goods wishes where a colony has
|
|
* requested that goods type. Used to retarget goods that have
|
|
* gone astray.
|
|
*/
|
|
private final java.util.Map<GoodsType, List<GoodsWish>> goodsWishes
|
|
= new HashMap<>();
|
|
|
|
/**
|
|
* A mapping of unit type to the worker wishes for that type.
|
|
* Used to allocate WishRealizationMissions for units.
|
|
*/
|
|
private final java.util.Map<UnitType, List<WorkerWish>> workerWishes
|
|
= new HashMap<>();
|
|
|
|
/**
|
|
* A mapping of contiguity number to number of wagons needed in
|
|
* that landmass.
|
|
*/
|
|
private final java.util.Map<Integer, Integer> wagonsNeeded
|
|
= new HashMap<>();
|
|
|
|
/** The colonies that start the turn badly defended. */
|
|
private final List<AIColony> badlyDefended = new ArrayList<>();
|
|
|
|
/**
|
|
* Current estimate of the number of new
|
|
* {@code BuildColonyMission}s to create.
|
|
*/
|
|
private int nBuilders = 0;
|
|
|
|
/**
|
|
* Current estimate of the number of new
|
|
* {@code PioneeringMission}s to create.
|
|
*/
|
|
private int nPioneers = 0;
|
|
|
|
/**
|
|
* Current estimate of the number of new
|
|
* {@code ScoutingMission}s to create.
|
|
*/
|
|
private int nScouts = 0;
|
|
|
|
/** Count of the number of transports needing a naval unit. */
|
|
private int nNavalCarrier = 0;
|
|
|
|
|
|
/**
|
|
* Creates a new {@code EuropeanAIPlayer}.
|
|
*
|
|
* @param aiMain The main AI-class.
|
|
* @param player The player that should be associated with this
|
|
* {@code AIPlayer}.
|
|
*/
|
|
public EuropeanAIPlayer(AIMain aiMain, Player player) {
|
|
super(aiMain, player);
|
|
}
|
|
|
|
/**
|
|
* Creates a new {@code AIPlayer}.
|
|
*
|
|
* @param aiMain The main AI-object.
|
|
* @param xr The input stream containing the XML.
|
|
* @throws XMLStreamException if a problem was encountered during parsing.
|
|
*/
|
|
public EuropeanAIPlayer(AIMain aiMain,
|
|
FreeColXMLReader xr) throws XMLStreamException {
|
|
super(aiMain, xr);
|
|
}
|
|
|
|
|
|
/**
|
|
* Checks if this player should use military units more aggressively.
|
|
*
|
|
* @return {@code true} if attacking other units and settlements are
|
|
* preferred above defending its own colonies.
|
|
*/
|
|
public boolean isAggressive() {
|
|
/*
|
|
* TODO: We need to decide where to put AI behavior parameters so that mod
|
|
* authors can customize the AI feel. Perhaps just in the specification?
|
|
*/
|
|
return getPlayer().getNation().getType().getId().equals("model.nationType.conquest")
|
|
|| getPlayer().getNation().getType().getId().equals("model.nationType.immigration");
|
|
}
|
|
|
|
/**
|
|
* Checks if this player should be attacking the natives.
|
|
*
|
|
* @return {@code true} if native settlements should be targeted by this player.
|
|
*/
|
|
public boolean isLikesAttackingNatives() {
|
|
return getPlayer().getNation().getType().getId().equals("model.nationType.conquest");
|
|
}
|
|
|
|
/**
|
|
* {@inheritDoc}
|
|
*/
|
|
@Override
|
|
public void removeAIObject(AIObject ao) {
|
|
if (ao instanceof AIColony) {
|
|
removeAIColony((AIColony)ao);
|
|
} else {
|
|
super.removeAIObject(ao);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Remove one of our colonies.
|
|
*
|
|
* @param aic The {@code AIColony} to remove.
|
|
*/
|
|
private void removeAIColony(AIColony aic) {
|
|
final Colony colony = aic.getColony();
|
|
|
|
Set<TileImprovementPlan> tips = new HashSet<>();
|
|
for (Tile t : colony.getOwnedTiles()) {
|
|
TileImprovementPlan tip = tipMap.remove(t);
|
|
if (tip != null) tips.add(tip);
|
|
}
|
|
|
|
for (AIGoods aig : aic.getExportGoods()) {
|
|
if (Map.isSameLocation(aig.getLocation(), colony)) {
|
|
aig.changeTransport(null);
|
|
aig.dispose();
|
|
}
|
|
}
|
|
|
|
transportDemand.remove(colony);
|
|
|
|
Set<Wish> wishes = new HashSet<>(aic.getWishes());
|
|
for (AIUnit aiu : getAIUnits()) {
|
|
PioneeringMission pm = aiu.getMission(PioneeringMission.class);
|
|
if (pm != null) {
|
|
if (tips.contains(pm.getTileImprovementPlan())) {
|
|
logger.info(pm + " collapses with loss of " + colony);
|
|
aiu.changeMission(null);
|
|
}
|
|
continue;
|
|
}
|
|
WishRealizationMission
|
|
wm = aiu.getMission(WishRealizationMission.class);
|
|
if (wm != null) {
|
|
if (wishes.contains(wm.getWish())) {
|
|
logger.info(wm + " collapses with loss of " + colony);
|
|
aiu.changeMission(null);
|
|
}
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Initialize the static fields that would be final but for
|
|
* needing the specification.
|
|
*
|
|
* @param spec The {@code Specification} to initialize from.
|
|
*/
|
|
private static synchronized void initializeFromSpecification(Specification spec) {
|
|
if (pioneerRole != null) return;
|
|
pioneerRole = spec.getRoleWithAbility(Ability.IMPROVE_TERRAIN, null);
|
|
scoutRole = spec.getRoleWithAbility(Ability.SPEAK_WITH_CHIEF, null);
|
|
liftBoycottCheatPercent
|
|
= spec.getInteger(GameOptions.LIFT_BOYCOTT_CHEAT);
|
|
equipScoutCheatPercent
|
|
= spec.getInteger(GameOptions.EQUIP_SCOUT_CHEAT);
|
|
equipPioneerCheatPercent
|
|
= spec.getInteger(GameOptions.EQUIP_PIONEER_CHEAT);
|
|
landUnitCheatPercent
|
|
= spec.getInteger(GameOptions.LAND_UNIT_CHEAT);
|
|
offensiveLandUnitCheatPercent
|
|
= spec.getInteger(GameOptions.OFFENSIVE_LAND_UNIT_CHEAT);
|
|
offensiveNavalUnitCheatPercent
|
|
= spec.getInteger(GameOptions.OFFENSIVE_NAVAL_UNIT_CHEAT);
|
|
transportNavalUnitCheatPercent
|
|
= spec.getInteger(GameOptions.TRANSPORT_NAVAL_UNIT_CHEAT);
|
|
}
|
|
|
|
/**
|
|
* Get the list of badly defended colonies.
|
|
*
|
|
* @return A list of {@code AIColony}s that were badly
|
|
* defended at the start of this turn.
|
|
*/
|
|
protected List<AIColony> getBadlyDefended() {
|
|
return badlyDefended;
|
|
}
|
|
|
|
/**
|
|
* Simple initialization of AI missions given that we know the starting
|
|
* conditions.
|
|
*
|
|
* @param lb A {@code LogBuilder} to log to.
|
|
*/
|
|
private void initializeMissions(LogBuilder lb) {
|
|
final AIMain aiMain = getAIMain();
|
|
List<AIUnit> aiUnits = getAIUnits();
|
|
lb.add("\n Initialize ");
|
|
|
|
// Find all the carriers with potential colony builders on board,
|
|
// give them missions.
|
|
final Map map = getGame().getMap();
|
|
final int maxRange = map.getWidth() + map.getHeight();
|
|
Location target;
|
|
Mission m;
|
|
TransportMission tm;
|
|
for (AIUnit aiCarrier : aiUnits) {
|
|
if (aiCarrier.hasMission()) continue;
|
|
Unit carrier = aiCarrier.getUnit();
|
|
if (!carrier.isNaval()) continue;
|
|
target = null;
|
|
for (Unit u : carrier.getUnitList()) {
|
|
AIUnit aiu = aiMain.getAIUnit(u);
|
|
for (int range = buildingRange; range < maxRange;
|
|
range += buildingRange) {
|
|
target = BuildColonyMission.findMissionTarget(aiu, range, false);
|
|
if (target != null) break;
|
|
}
|
|
if (target == null) {
|
|
throw new RuntimeException("Initial colony fail: " + u);
|
|
}
|
|
if ((m = getBuildColonyMission(aiu, target)) != null) {
|
|
lb.add(m, ", ");
|
|
}
|
|
}
|
|
// Initialize the carrier mission after the cargo units
|
|
// have a valid mission so that the transport list and
|
|
// mission target do not break.
|
|
tm = (TransportMission)getTransportMission(aiCarrier);
|
|
if (tm != null) {
|
|
lb.add(tm);
|
|
for (Unit u : carrier.getUnitList()) {
|
|
AIUnit aiu = getAIMain().getAIUnit(u);
|
|
if (aiu == null) continue;
|
|
tm.queueTransportable(aiu, false, lb);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Put in some backup missions.
|
|
lb.mark();
|
|
for (AIUnit aiu : aiUnits) {
|
|
if (aiu.hasMission()) continue;
|
|
if ((m = getSimpleMission(aiu)) != null) lb.add(m, ", ");
|
|
}
|
|
if (lb.grew("\n Backup: ")) lb.shrink(", ");
|
|
}
|
|
|
|
/**
|
|
* Cheat by adding gold to guarantee the player has a minimum amount.
|
|
*
|
|
* @param amount The minimum amount of gold required.
|
|
* @param lb A {@code LogBuilder} to log to.
|
|
*/
|
|
public void cheatGold(int amount, LogBuilder lb) {
|
|
final Player player = getPlayer();
|
|
int gold = player.getGold();
|
|
if (gold < amount) {
|
|
amount -= gold;
|
|
player.modifyGold(amount);
|
|
lb.add("added ", amount, " gold");
|
|
}
|
|
player.logCheat(amount + " gold");
|
|
}
|
|
|
|
/**
|
|
* Cheats for the AI. Please try to centralize cheats here.
|
|
*
|
|
* FIXME: Remove when the AI is good enough.
|
|
*
|
|
* @param lb A {@code LogBuilder} to log to.
|
|
*/
|
|
private void cheat(LogBuilder lb) {
|
|
final AIMain aiMain = getAIMain();
|
|
if (!aiMain.getFreeColServer().getSinglePlayer()) return;
|
|
|
|
final Player player = getPlayer();
|
|
if (player.getPlayerType() != PlayerType.COLONIAL) return;
|
|
lb.mark();
|
|
|
|
final Specification spec = getSpecification();
|
|
final Game game = getGame();
|
|
final Market market = player.getMarket();
|
|
final Europe europe = player.getEurope();
|
|
final Random air = getAIRandom();
|
|
final List<GoodsType> arrears = new ArrayList<>();
|
|
if (market != null) {
|
|
arrears.addAll(transform(spec.getGoodsTypeList(),
|
|
gt -> market.getArrears(gt) > 0));
|
|
}
|
|
final int nCheats = arrears.size() + 6; // 6 cheats + arrears
|
|
int[] randoms = randomInts(logger, "cheats", air, 100, nCheats);
|
|
int cheatIndex = 0;
|
|
|
|
for (GoodsType goodsType : arrears) {
|
|
if (randoms[cheatIndex++] < liftBoycottCheatPercent) {
|
|
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.
|
|
final CachingFunction<Colony, Modifier> partyModifierMapper
|
|
= new CachingFunction<>(c ->
|
|
first(transform(c.getModifiers(), partyPred)));
|
|
Colony party = getRandomMember(logger, "end boycott",
|
|
transform(player.getColonies(),
|
|
isNotNull(c -> partyModifierMapper.apply(c))),
|
|
air);
|
|
if (party != null) {
|
|
party.removeModifier(partyModifierMapper.apply(party));
|
|
lb.add("lift-boycott at ", party, ", ");
|
|
player.logCheat("lift boycott at " + party.getName());
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!europe.isEmpty()
|
|
&& scoutsNeeded() > 0
|
|
&& randoms[cheatIndex++] < equipScoutCheatPercent) {
|
|
for (Unit u : transform(europe.getUnits(), equipPred)) {
|
|
try {
|
|
int g = europe.priceGoods(u.getGoodsDifference(scoutRole, 1));
|
|
cheatGold(g, lb);
|
|
} catch (FreeColException fce) {
|
|
continue;
|
|
}
|
|
if (getAIUnit(u).equipForRole(spec.getRoleWithAbility(Ability.SPEAK_WITH_CHIEF, null))) {
|
|
lb.add(" to equip scout ", u, ", ");
|
|
player.logCheat("Equip scout " + u.toShortString());
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!europe.isEmpty()
|
|
&& pioneersNeeded() > 0
|
|
&& randoms[cheatIndex++] < equipPioneerCheatPercent) {
|
|
for (Unit u : transform(europe.getUnits(), equipPred)) {
|
|
try {
|
|
int g = europe.priceGoods(u.getGoodsDifference(pioneerRole, 1));
|
|
cheatGold(g, lb);
|
|
} catch (FreeColException fce) {
|
|
continue;
|
|
}
|
|
if (getAIUnit(u).equipForRole(spec.getRoleWithAbility(Ability.IMPROVE_TERRAIN, null))) {
|
|
lb.add(" to equip pioneer ", u, ", ");
|
|
player.logCheat("Equip pioneer " + u.toShortString());
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (randoms[cheatIndex++] < landUnitCheatPercent) {
|
|
final Predicate<Entry<UnitType, List<WorkerWish>>> bestWishPred = e -> {
|
|
UnitType ut = e.getKey();
|
|
return ut != null && ut.isAvailableTo(player)
|
|
&& europe.getUnitPrice(ut) != UNDEFINED
|
|
&& any(e.getValue());
|
|
};
|
|
WorkerWish bestWish = maximize(transform(workerWishes.entrySet(),
|
|
bestWishPred,
|
|
e -> first(e.getValue()),
|
|
Collectors.toSet()),
|
|
ValuedAIObject.ascendingValueComparator);
|
|
int cost = (bestWish != null)
|
|
? europe.getUnitPrice(bestWish.getUnitType())
|
|
: (player.getImmigration() < player.getImmigrationRequired() / 2)
|
|
? player.getEuropeanRecruitPrice()
|
|
: INFINITY;
|
|
if (cost != INFINITY) {
|
|
cheatGold(cost, lb);
|
|
AIUnit aiu;
|
|
if (bestWish == null) {
|
|
if ((aiu = recruitAIUnitInEurope(-1)) != null) {
|
|
// let giveNormalMissions look after the mission
|
|
lb.add(" to recruit ", aiu.getUnit(), ", ");
|
|
}
|
|
} else {
|
|
if ((aiu = trainAIUnitInEurope(bestWish.getUnitType())) != null) {
|
|
Mission m = getWishRealizationMission(aiu, bestWish);
|
|
if (m != null) {
|
|
lb.add(" to train for ", m, ", ");
|
|
} else {
|
|
lb.add(" to train ", aiu.getUnit(), ", ");
|
|
}
|
|
}
|
|
}
|
|
if (aiu != null) player.logCheat("Make " + aiu.getUnit());
|
|
}
|
|
}
|
|
|
|
if (game.getTurn().getNumber() > 300
|
|
&& player.isAtWar()
|
|
&& randoms[cheatIndex++] < offensiveLandUnitCheatPercent) {
|
|
// - collect enemies, prefer not to antagonize the strong or
|
|
// crush the weak
|
|
List<Player> wars = transform(game.getLivePlayers(player),
|
|
x -> player.atWarWith(x));
|
|
List<Player> preferred = new ArrayList<>(wars.size());
|
|
List<Player> enemies = new ArrayList<>(wars.size());
|
|
for (Player p : wars) {
|
|
enemies.add(p);
|
|
double strength = getStrengthRatio(p);
|
|
if (strength < 3.0/2.0 && strength > 2.0/3.0) {
|
|
preferred.add(p);
|
|
}
|
|
}
|
|
if (!preferred.isEmpty()) {
|
|
enemies.clear();
|
|
enemies.addAll(preferred);
|
|
}
|
|
List<Colony> colonies = player.getColonyList();
|
|
// Find a target to attack.
|
|
Location target = null;
|
|
// Few colonies? Attack the weakest European port
|
|
if (colonies.size() < 3) {
|
|
final Comparator<Colony> targetScore
|
|
= cachingDoubleComparator(c -> {
|
|
double score = 100000.0 / c.getUnitCount();
|
|
Building stockade = c.getStockade();
|
|
return (stockade == null) ? 1.0
|
|
: score / (stockade.getLevel() + 1.5);
|
|
});
|
|
target = maximize(flatten(enemies, Player::isEuropean,
|
|
Player::getConnectedPorts),
|
|
targetScore);
|
|
}
|
|
// Otherwise attack something near a weak colony
|
|
if (target == null && !colonies.isEmpty()) {
|
|
List<AIColony> bad = new ArrayList<>(getBadlyDefended());
|
|
if (bad.isEmpty()) bad.addAll(getAIColonies());
|
|
AIColony defend = getRandomMember(logger,
|
|
"AIColony to defend", bad, air);
|
|
Tile center = defend.getColony().getTile();
|
|
Tile t = game.getMap().searchCircle(center,
|
|
GoalDeciders.getEnemySettlementGoalDecider(enemies),
|
|
30);
|
|
if (t != null) target = t.getSettlement();
|
|
}
|
|
if (target != null) {
|
|
List<AbstractUnit> aMercs = new ArrayList<>();
|
|
int aPrice = player.getMonarch().loadMercenaries(air, aMercs);
|
|
if (aPrice > 0) {
|
|
List<Unit> mercs = ((ServerPlayer)player)
|
|
.createUnits(aMercs, europe, air);
|
|
for (Unit u : mercs) {
|
|
AIUnit aiu = getAIUnit(u);
|
|
if (aiu == null) continue; // Can not happen
|
|
player.logCheat("Enlist " + aiu.getUnit());
|
|
Mission m = getSeekAndDestroyMission(aiu, target);
|
|
if (m != null) {
|
|
lb.add("enlisted ", m, ", ");
|
|
} else {
|
|
lb.add("enlisted ", aiu.getUnit(), ", ");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Always cheat a new armed ship if the navy is destroyed,
|
|
// otherwise if the navy is below average the chance to cheat
|
|
// is proportional to how badly below average.
|
|
double naval = getNavalStrengthRatio();
|
|
int nNaval = (player.getUnitCount(true) == 0) ? 100
|
|
: (0.0f < naval && naval < 0.5f)
|
|
? (int)(naval * offensiveNavalUnitCheatPercent)
|
|
: -1;
|
|
final Function<UnitType, RandomChoice<UnitType>> mapper = ut ->
|
|
new RandomChoice<>(ut, 100000 / europe.getUnitPrice(ut));
|
|
if (randoms[cheatIndex++] < nNaval) {
|
|
cheatUnit(transform(spec.getUnitTypeList(),
|
|
ut -> ut.hasAbility(Ability.NAVAL_UNIT)
|
|
&& ut.isAvailableTo(player)
|
|
&& ut.hasPrice()
|
|
&& ut.isOffensive(),
|
|
mapper), "offensive-naval", lb);
|
|
}
|
|
// Only cheat carriers if they have work to do.
|
|
int nCarrier = (nNavalCarrier > 0) ? transportNavalUnitCheatPercent
|
|
: -1;
|
|
if (randoms[cheatIndex++] < nCarrier) {
|
|
cheatUnit(transform(spec.getUnitTypeList(),
|
|
ut -> ut.hasAbility(Ability.NAVAL_UNIT)
|
|
&& ut.isAvailableTo(player)
|
|
&& ut.hasPrice()
|
|
&& ut.getSpace() > 0,
|
|
mapper), "transport-naval", lb);
|
|
}
|
|
|
|
if (lb.grew("\n Cheats: ")) lb.shrink(", ");
|
|
}
|
|
|
|
/**
|
|
* Cheat-build a unit in Europe.
|
|
*
|
|
* @param rc A list of random choices to choose from.
|
|
* @param what A description of the unit.
|
|
* @param lb A {@code LogBuilder} to log to.
|
|
* @return The {@code AIUnit} built.
|
|
*/
|
|
private AIUnit cheatUnit(List<RandomChoice<UnitType>> rc, String what,
|
|
LogBuilder lb) {
|
|
UnitType unitToPurchase
|
|
= RandomChoice.getWeightedRandom(logger, "Cheat which unit",
|
|
rc, getAIRandom());
|
|
return (unitToPurchase == null) ? null
|
|
: cheatUnit(unitToPurchase, what, lb);
|
|
}
|
|
|
|
/**
|
|
* Cheat-build a unit in Europe.
|
|
*
|
|
* @param unitType The {@code UnitType} to build.
|
|
* @param what A description of the unit.
|
|
* @param lb A {@code LogBuilder} to log to.
|
|
* @return The {@code AIUnit} built.
|
|
*/
|
|
private AIUnit cheatUnit(UnitType unitType, String what, LogBuilder lb) {
|
|
final Player player = getPlayer();
|
|
final Europe europe = player.getEurope();
|
|
int cost = europe.getUnitPrice(unitType);
|
|
cheatGold(cost, lb);
|
|
AIUnit result = trainAIUnitInEurope(unitType);
|
|
lb.add(" to build ", what, " ", unitType.getSuffix(),
|
|
((result != null) ? "" : "(failed)"), ", ");
|
|
if (result == null) return null;
|
|
player.logCheat("Build " + result.getUnit());
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Assign transportable units and goods to available carriers.
|
|
*
|
|
* These supply driven assignments supplement the demand driven
|
|
* calls inside TransportMission.
|
|
*
|
|
* @param transportables A list of {@code TransportableAIObject}s to
|
|
* allocated transport for.
|
|
* @param missions A list of {@code TransportMission}s to potentially
|
|
* assign more transportables to.
|
|
* @param lb A {@code LogBuilder} to log to.
|
|
*/
|
|
public void allocateTransportables(List<TransportableAIObject> transportables,
|
|
List<TransportMission> missions,
|
|
LogBuilder lb) {
|
|
if (transportables.isEmpty()) return;
|
|
if (missions.isEmpty()) return;
|
|
|
|
lb.add("\n Allocate Transport cargo=", transportables.size(),
|
|
" carriers=", missions.size());
|
|
//for (TransportableAIObject t : urgent) lb.add(" ", t);
|
|
//lb.add(" ->");
|
|
//for (Mission m : missions) lb.add(" ", m);
|
|
|
|
LogBuilder lb2 = new LogBuilder(0);
|
|
TransportMission best;
|
|
float bestValue;
|
|
boolean present;
|
|
int i = 0;
|
|
outer: while (i < transportables.size()) {
|
|
if (missions.isEmpty()) break;
|
|
TransportableAIObject t = transportables.get(i);
|
|
lb.add(" for ", t);
|
|
best = null;
|
|
bestValue = 0.0f;
|
|
present = false;
|
|
for (TransportMission tm : missions) {
|
|
if (!tm.spaceAvailable(t)) continue;
|
|
Cargo cargo = tm.makeCargo(t, lb2);
|
|
if (cargo == null) { // Serious problem with this cargo
|
|
transportables.remove(i);
|
|
continue outer;
|
|
}
|
|
int turns = cargo.getTurns();
|
|
float value;
|
|
if (turns == 0) {
|
|
value = tm.destinationCapacity();
|
|
if (!present) bestValue = 0.0f; // reset
|
|
present = true;
|
|
} else {
|
|
value = (present) ? -1.0f
|
|
: (float)t.getTransportPriority() / turns;
|
|
}
|
|
if (bestValue < value) {
|
|
bestValue = value;
|
|
best = tm;
|
|
}
|
|
}
|
|
if (best == null) {
|
|
lb.add(" nothing found");
|
|
} else {
|
|
lb.add(" ", best.getUnit(), " chosen");
|
|
if (best.queueTransportable(t, false, lb)) {
|
|
claimTransportable(t);
|
|
if (best.destinationCapacity() <= 0) {
|
|
missions.remove(best);
|
|
}
|
|
} else {
|
|
missions.remove(best);
|
|
}
|
|
}
|
|
i++;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Brings gifts to nice players with nearby colonies.
|
|
*
|
|
* FIXME: 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.
|
|
*
|
|
* @param lb A {@code LogBuilder} to log to.
|
|
*/
|
|
private void bringGifts(@SuppressWarnings("unused") LogBuilder lb) {
|
|
return;
|
|
}
|
|
|
|
/**
|
|
* Demands goods from players with nearby colonies.
|
|
*
|
|
* FIXME: European players can also demand tribute!
|
|
*
|
|
* @param lb A {@code LogBuilder} to log to.
|
|
*/
|
|
private void demandTribute(@SuppressWarnings("unused") LogBuilder lb) {
|
|
return;
|
|
}
|
|
|
|
|
|
// Tile Improvement handling
|
|
|
|
/**
|
|
* Rebuilds a map of locations to TileImprovementPlans.
|
|
*
|
|
* Called by startWorking at the start of every turn.
|
|
*
|
|
* Public for the test suite.
|
|
*
|
|
* @param lb A {@code LogBuilder} to log to.
|
|
*/
|
|
public void buildTipMap(LogBuilder lb) {
|
|
tipMap.clear();
|
|
for (AIColony aic : getAIColonies()) {
|
|
for (TileImprovementPlan tip : aic.getTileImprovementPlans()) {
|
|
if (tip == null || tip.isComplete()) {
|
|
aic.removeTileImprovementPlan(tip);
|
|
} else if (tip.getPioneer() != null) {
|
|
// Do nothing, remove when complete
|
|
} else if (!tip.validate()) {
|
|
aic.removeTileImprovementPlan(tip);
|
|
tip.dispose();
|
|
} else if (tip.getTarget() == null) {
|
|
logger.warning("No target for tip: " + tip);
|
|
} else {
|
|
TileImprovementPlan other = tipMap.get(tip.getTarget());
|
|
if (other == null || other.getValue() < tip.getValue()) {
|
|
tipMap.put(tip.getTarget(), tip);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (!tipMap.isEmpty()) {
|
|
lb.add("\n Improvements:");
|
|
forEachMapEntry(tipMap, e -> {
|
|
Tile t = e.getKey();
|
|
TileImprovementPlan tip = e.getValue();
|
|
AIUnit pioneer = tip.getPioneer();
|
|
lb.add(" ", t, "=", tip.getType().getSuffix());
|
|
if (pioneer != null) lb.add("/", pioneer.getUnit());
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Update the tip map with tips from a new colony.
|
|
*
|
|
* @param aic The new {@code AIColony}.
|
|
*/
|
|
private void updateTipMap(AIColony aic) {
|
|
for (TileImprovementPlan tip : aic.getTileImprovementPlans()) {
|
|
tipMap.put(tip.getTarget(), tip);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Gets the best plan for a tile from the tipMap.
|
|
*
|
|
* @param tile The {@code Tile} 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} to check.
|
|
* @return The tile with the best plan for a colony, or null if none found.
|
|
*/
|
|
public Tile getBestPlanTile(Colony colony) {
|
|
final Comparator<TileImprovementPlan> valueComp
|
|
= Comparator.comparingInt(TileImprovementPlan::getValue);
|
|
final Function<Tile, TileImprovementPlan> tileMapper = t ->
|
|
tipMap.get(t);
|
|
TileImprovementPlan best
|
|
= maximize(map(colony.getOwnedTiles(), tileMapper),
|
|
isNotNull(), valueComp);
|
|
return (best == null) ? null : best.getTarget();
|
|
}
|
|
|
|
/**
|
|
* Remove a {@code TileImprovementPlan} from the relevant colony.
|
|
*
|
|
* @param plan The {@code TileImprovementPlan} to remove.
|
|
*/
|
|
public void removeTileImprovementPlan(TileImprovementPlan plan) {
|
|
if (plan == null) return;
|
|
if (plan.getTarget() != null) tipMap.remove(plan.getTarget());
|
|
for (AIColony aic : getAIColonies()) {
|
|
if (aic.removeTileImprovementPlan(plan)) break;
|
|
}
|
|
}
|
|
|
|
|
|
// Transport handling
|
|
|
|
/**
|
|
* Update the transport of a unit following a target change.
|
|
*
|
|
* If the target has changed
|
|
* - drop all non-boarded transport unless the target is the same
|
|
* - dump boarded transport with no target
|
|
* - requeue all boarded transport unless the target is the same
|
|
*
|
|
* @param aiu The {@code AIUnit} to check.
|
|
* @param oldTarget The old target {@code Location}.
|
|
* @param lb A {@code LogBuilder} to log to.
|
|
*/
|
|
public void updateTransport(AIUnit aiu, Location oldTarget, LogBuilder lb) {
|
|
final AIUnit aiCarrier = aiu.getTransport();
|
|
final Mission newMission = aiu.getMission();
|
|
final Location newTarget = (newMission == null) ? null
|
|
: newMission.getTarget();
|
|
TransportMission tm;
|
|
if (aiCarrier != null
|
|
&& (tm = aiCarrier.getMission(TransportMission.class)) != null
|
|
&& !Map.isSameLocation(oldTarget, newTarget)) {
|
|
if (aiu.getUnit().getLocation() != aiCarrier.getUnit()) {
|
|
lb.add(", drop transport ", aiCarrier.getUnit());
|
|
aiu.dropTransport();
|
|
} else if (newTarget == null) {
|
|
tm.dumpTransportable(aiu, lb);
|
|
} else {
|
|
tm.requeueTransportable(aiu, lb);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Checks if a transportable needs transport.
|
|
*
|
|
* @param t The {@code TransportableAIObject} to check.
|
|
* @return True if no transport is already present or the
|
|
* transportable is already aboard a carrier, and there is a
|
|
* well defined source and destination location.
|
|
*/
|
|
private boolean requestsTransport(TransportableAIObject t) {
|
|
return t.getTransport() == null
|
|
&& t.getTransportDestination() != null
|
|
&& t.getTransportSource() != null
|
|
&& !(t.getLocation() instanceof Unit);
|
|
}
|
|
|
|
/**
|
|
* Checks that the carrier assigned to a transportable is has a
|
|
* transport mission and the transport is queued thereon.
|
|
*
|
|
* @param t The {@code TransportableAIObject} to check.
|
|
* @return True if all is well.
|
|
*/
|
|
private boolean checkTransport(TransportableAIObject t) {
|
|
AIUnit aiCarrier = t.getTransport();
|
|
if (aiCarrier == null) return false;
|
|
TransportMission tm = aiCarrier.getMission(TransportMission.class);
|
|
if (tm != null && tm.isTransporting(t)) return true;
|
|
t.changeTransport(null);
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Changes the needed wagons map for a specified tile/contiguity.
|
|
* If the change is zero, that is a special flag that a connected
|
|
* port is available, and thus that the map should be initialized
|
|
* for that contiguity.
|
|
*
|
|
* @param tile The {@code Tile} to derive the contiguity from.
|
|
* @param amount The change to make.
|
|
*/
|
|
private void changeNeedWagon(Tile tile, int amount) {
|
|
if (tile == null) return;
|
|
int contig = tile.getContiguity();
|
|
if (contig > 0) {
|
|
Integer i = wagonsNeeded.get(contig);
|
|
if (i == null) {
|
|
if (amount == 0) wagonsNeeded.put(contig, 0);
|
|
} else {
|
|
wagonsNeeded.put(contig, i + amount);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Rebuild the transport maps.
|
|
* Count the number of transports requiring naval/land carriers.
|
|
*
|
|
* @param lb A {@code LogBuilder} to log to.
|
|
*/
|
|
private void buildTransportMaps(LogBuilder lb) {
|
|
transportDemand.clear();
|
|
transportSupply.clear();
|
|
wagonsNeeded.clear();
|
|
nNavalCarrier = 0;
|
|
|
|
// Prime the wagonsNeeded map with contiguities with a connected port
|
|
for (AIColony aic : getAIColonies()) {
|
|
Colony colony = aic.getColony();
|
|
if (colony.isConnectedPort()) changeNeedWagon(colony.getTile(), 0);
|
|
}
|
|
|
|
for (AIUnit aiu : getAIUnits()) {
|
|
if (aiu.hasMission() && !aiu.getMission().isValid()) continue;
|
|
Unit u = aiu.getUnit();
|
|
if (u.isCarrier()) {
|
|
if (u.isNaval()) {
|
|
nNavalCarrier--;
|
|
} else {
|
|
changeNeedWagon(u.getTile(), -1);
|
|
}
|
|
} else {
|
|
checkTransport(aiu);
|
|
if (requestsTransport(aiu)) {
|
|
transportSupply.add(aiu);
|
|
aiu.incrementTransportPriority();
|
|
nNavalCarrier++;
|
|
}
|
|
}
|
|
}
|
|
|
|
for (AIColony aic : getAIColonies()) {
|
|
for (AIGoods aig : aic.getExportGoods()) {
|
|
checkTransport(aig);
|
|
if (requestsTransport(aig)) {
|
|
transportSupply.add(aig);
|
|
aig.incrementTransportPriority();
|
|
Location src = aig.getTransportSource();
|
|
Location dst = aig.getTransportDestination();
|
|
if (!Map.isSameContiguity(src, dst)) {
|
|
nNavalCarrier++;
|
|
}
|
|
}
|
|
}
|
|
Colony colony = aic.getColony();
|
|
if (!colony.isConnectedPort()) {
|
|
changeNeedWagon(colony.getTile(), 1);
|
|
}
|
|
}
|
|
|
|
for (Wish w : getWishes()) {
|
|
TransportableAIObject t = w.getTransportable();
|
|
if (t != null && t.getTransport() == null
|
|
&& t.getTransportDestination() != null) {
|
|
Location loc = Location.upLoc(t.getTransportDestination());
|
|
appendToMapList(transportDemand, loc, w);
|
|
}
|
|
}
|
|
|
|
if (!transportSupply.isEmpty()) {
|
|
lb.add("\n Transport Supply:");
|
|
for (TransportableAIObject t : transportSupply) {
|
|
lb.add(" ", t.getTransportPriority(), "+", t);
|
|
}
|
|
}
|
|
if (!transportDemand.isEmpty()) {
|
|
lb.add("\n Transport Demand:");
|
|
forEachMapEntry(transportDemand, e -> {
|
|
Location ld = e.getKey();
|
|
lb.add("\n ", ld, "[");
|
|
for (Wish w : e.getValue()) lb.add(" ", w);
|
|
lb.add(" ]");
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Gets all transportables sorted by values.
|
|
*
|
|
* @return The transportables in descending order.
|
|
*/
|
|
public List<TransportableAIObject> getTransportables() {
|
|
return sort(transportSupply, ValuedAIObject.descendingValueComparator);
|
|
}
|
|
|
|
/**
|
|
* Gets the most urgent transportables.
|
|
*
|
|
* @return The most urgent of the available transportables.
|
|
*/
|
|
public List<TransportableAIObject> getUrgentTransportables() {
|
|
/*
|
|
List<TransportableAIObject> urgent
|
|
= sort(transportSupply, ValuedAIObject.descendingValueComparator);
|
|
// Do not let the list exceed 10% of all transports
|
|
int urge = urgent.size();
|
|
urge = Math.max(2, (urge + 5) / 10);
|
|
while (urgent.size() > urge) urgent.remove(urge);
|
|
return urgent;
|
|
*/
|
|
|
|
/*
|
|
* Deactived the code above for now since I cannot detect any difference
|
|
* when activated ... and if we activate it again, please use something
|
|
* like this instead::
|
|
*
|
|
* final int urgentNumber = Math.max(2, (urgent.size() + 5) / 10;
|
|
* return urgent.subList(0, Math.min(urgent.size(), urgentNumber));
|
|
*/
|
|
|
|
return List.of();
|
|
}
|
|
|
|
/**
|
|
* Allows a TransportMission to signal that it has taken responsibility
|
|
* for a TransportableAIObject.
|
|
*
|
|
* @param t The {@code TransportableAIObject} being claimed.
|
|
* @return True if the transportable was claimed from the supply map.
|
|
*/
|
|
public boolean claimTransportable(TransportableAIObject t) {
|
|
return transportSupply.remove(t);
|
|
}
|
|
|
|
/**
|
|
* Rearrange colonies.
|
|
*
|
|
* @param lb A {@code LogBuilder} to log to.
|
|
*/
|
|
private void rearrangeColonies(LogBuilder lb) {
|
|
for (AIColony aic : getAIColonies()) aic.rearrangeColony(lb);
|
|
}
|
|
|
|
|
|
// Wish handling
|
|
|
|
/**
|
|
* Suppress European trade in a goods type. A goods party and
|
|
* boycott is incoming.
|
|
*
|
|
* @param type The {@code GoodsType} to suppress.
|
|
* @param lb A {@code LogBuilder} to log to.
|
|
*/
|
|
private void suppressEuropeanTrade(GoodsType type, LogBuilder lb) {
|
|
final Player player = getPlayer();
|
|
final Europe europe = player.getEurope();
|
|
|
|
lb.add(" Suppressing trade in ", type.getSuffix());
|
|
List<Unit> units = new ArrayList<>(europe.getUnitList());
|
|
units.addAll(player.getHighSeas().getUnitList());
|
|
for (Unit u : units) {
|
|
int amount;
|
|
AIUnit aiu;
|
|
if (u.isCarrier() && (amount = u.getGoodsCount(type)) > 0
|
|
&& (aiu = getAIUnit(u)) != null
|
|
&& AIMessage.askUnloadGoods(type, amount, aiu)) {
|
|
lb.add(", ", u, " sold ", amount);
|
|
}
|
|
}
|
|
for (AIUnit aiu : getAIUnits()) {
|
|
TransportMission tm = aiu.getMission(TransportMission.class);
|
|
if (tm != null) tm.suppressEuropeanTrade(type, lb);
|
|
}
|
|
|
|
int n = 0;
|
|
List<GoodsWish> wishes = goodsWishes.get(type);
|
|
if (wishes != null) {
|
|
for (GoodsWish gw : wishes) {
|
|
if (gw.getGoodsType() == type
|
|
&& gw.getDestination() == europe) {
|
|
if (gw.getTransportable() instanceof AIGoods) {
|
|
AIGoods aig = (AIGoods)gw.getTransportable();
|
|
consumeGoodsWish(aig, gw);
|
|
aig.setTransportDestination(null);
|
|
}
|
|
gw.dispose();
|
|
n++;
|
|
}
|
|
}
|
|
if (n > 0) lb.add(", dropped ", n, " goods wishes");
|
|
}
|
|
lb.add(".");
|
|
}
|
|
|
|
/**
|
|
* Gets a list of the wishes at a given location for a unit type.
|
|
*
|
|
* @param loc The {@code Location} to look for wishes at.
|
|
* @param type The {@code UnitType} to look for.
|
|
* @return A list of {@code WorkerWish}es.
|
|
*/
|
|
public List<WorkerWish> getWorkerWishesAt(Location loc, UnitType type) {
|
|
List<Wish> demand = transportDemand.get(Location.upLoc(loc));
|
|
return (demand == null) ? Collections.<WorkerWish>emptyList()
|
|
: transform(demand,
|
|
w -> w instanceof WorkerWish
|
|
&& ((WorkerWish)w).getUnitType() == type,
|
|
w -> (WorkerWish)w);
|
|
}
|
|
|
|
/**
|
|
* Gets a list of the wishes at a given location for a goods type.
|
|
*
|
|
* @param loc The {@code Location} to look for wishes at.
|
|
* @param type The {@code GoodsType} to look for.
|
|
* @return A list of {@code GoodsWish}es.
|
|
*/
|
|
public List<GoodsWish> getGoodsWishesAt(Location loc, GoodsType type) {
|
|
List<Wish> demand = transportDemand.get(Location.upLoc(loc));
|
|
return (demand == null) ? Collections.<GoodsWish>emptyList()
|
|
: transform(demand,
|
|
w -> w instanceof GoodsWish
|
|
&& ((GoodsWish)w).getGoodsType() == type,
|
|
w -> (GoodsWish)w);
|
|
}
|
|
|
|
/**
|
|
* Gets the best worker wish for a carrier unit.
|
|
*
|
|
* @param aiUnit The carrier {@code AIUnit}.
|
|
* @param unitType The {@code UnitType} to find a wish for.
|
|
* @return The best worker wish for the unit.
|
|
*/
|
|
private WorkerWish getBestWorkerWish(AIUnit aiUnit, UnitType unitType) {
|
|
List<WorkerWish> wishes = workerWishes.get(unitType);
|
|
if (wishes == null) return null;
|
|
|
|
final Unit carrier = aiUnit.getUnit();
|
|
WorkerWish carried = null;
|
|
WorkerWish other = null;
|
|
double bestCarriedValue = -1.0, bestOtherValue = -1.0;
|
|
for (WorkerWish w : wishes) {
|
|
Location dest = w.getDestination();
|
|
if (dest == null) continue; // Defend against crash
|
|
int turns = carrier.getTurnsToReach(dest);
|
|
if (turns < Unit.MANY_TURNS) {
|
|
if (bestCarriedValue < (double)w.getValue() / turns) {
|
|
bestCarriedValue = (double)w.getValue() / turns;
|
|
carried = w;
|
|
}
|
|
} else {
|
|
if (bestOtherValue < w.getValue()) {
|
|
bestOtherValue = w.getValue();
|
|
other = w;
|
|
}
|
|
}
|
|
}
|
|
return (carried != null) ? carried : (other != null) ? other : null;
|
|
}
|
|
|
|
/**
|
|
* Gets the best goods wish for a carrier unit.
|
|
*
|
|
* @param aiUnit The carrier {@code AIUnit}.
|
|
* @param goodsType The {@code GoodsType} to wish for.
|
|
* @return The best {@code GoodsWish} for the unit.
|
|
*/
|
|
public GoodsWish getBestGoodsWish(AIUnit aiUnit, GoodsType goodsType) {
|
|
final Unit carrier = aiUnit.getUnit();
|
|
final ToDoubleFunction<GoodsWish> wishValue
|
|
= cacheDouble(gw -> {
|
|
int turns = carrier.getTurnsToReach(carrier.getLocation(),
|
|
gw.getDestination());
|
|
return (turns >= Unit.MANY_TURNS) ? -1.0
|
|
: (double)gw.getValue() / turns;
|
|
});
|
|
final Comparator<GoodsWish> comp
|
|
= Comparator.comparingDouble(wishValue);
|
|
|
|
List<GoodsWish> wishes = goodsWishes.get(goodsType);
|
|
return (wishes == null) ? null
|
|
: maximize(wishes, gw -> wishValue.applyAsDouble(gw) > 0.0, comp);
|
|
}
|
|
|
|
/**
|
|
* Rebuilds the goods and worker wishes maps.
|
|
*
|
|
* @param lb A {@code LogBuilder} to log to.
|
|
*/
|
|
private void buildWishMaps(LogBuilder lb) {
|
|
for (UnitType unitType : getSpecification().getUnitTypeList()) {
|
|
List<WorkerWish> wl = workerWishes.get(unitType);
|
|
if (wl == null) {
|
|
workerWishes.put(unitType, new ArrayList<WorkerWish>());
|
|
} else {
|
|
wl.clear();
|
|
}
|
|
}
|
|
for (GoodsType goodsType : getSpecification().getStorableGoodsTypeList()) {
|
|
List<GoodsWish> gl = goodsWishes.get(goodsType);
|
|
if (gl == null) {
|
|
goodsWishes.put(goodsType, new ArrayList<GoodsWish>());
|
|
} else {
|
|
gl.clear();
|
|
}
|
|
}
|
|
|
|
for (Wish w : getWishes()) {
|
|
if (w instanceof WorkerWish) {
|
|
WorkerWish ww = (WorkerWish)w;
|
|
if (ww.getTransportable() == null) {
|
|
appendToMapList(workerWishes, ww.getUnitType(), ww);
|
|
}
|
|
} else if (w instanceof GoodsWish) {
|
|
GoodsWish gw = (GoodsWish)w;
|
|
if (gw.getDestination() instanceof Colony) {
|
|
appendToMapList(goodsWishes, gw.getGoodsType(), gw);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!workerWishes.isEmpty()) {
|
|
lb.add("\n Wishes (workers):");
|
|
forEachMapEntry(workerWishes, e -> {
|
|
UnitType ut = e.getKey();
|
|
List<WorkerWish> wl = e.getValue();
|
|
if (!wl.isEmpty()) {
|
|
lb.add("\n ", ut.getSuffix(), ":");
|
|
for (WorkerWish ww : wl) {
|
|
lb.add(" ", ww.getDestination(),
|
|
"(", ww.getValue(), ")");
|
|
}
|
|
}
|
|
});
|
|
}
|
|
if (!goodsWishes.isEmpty()) {
|
|
lb.add("\n Wishes (goods):");
|
|
forEachMapEntry(goodsWishes, e -> {
|
|
GoodsType gt = e.getKey();
|
|
List<GoodsWish> gl = e.getValue();
|
|
if (!gl.isEmpty()) {
|
|
lb.add("\n ", gt.getSuffix(), ":");
|
|
for (GoodsWish gw : gl) {
|
|
lb.add(" ", gw.getDestination(),
|
|
"(", gw.getValue(), ")");
|
|
}
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Consume a WorkerWish, yielding a WishRealizationMission for a unit.
|
|
*
|
|
* @param aiUnit The {@code AIUnit} to check.
|
|
* @param ww The {@code WorkerWish} to consume.
|
|
*/
|
|
private void consumeWorkerWish(AIUnit aiUnit, WorkerWish ww) {
|
|
final Unit unit = aiUnit.getUnit();
|
|
List<WorkerWish> wwL = workerWishes.get(unit.getType());
|
|
wwL.remove(ww);
|
|
List<Wish> wl = transportDemand.get(ww.getDestination());
|
|
if (wl != null) wl.remove(ww);
|
|
ww.setTransportable(aiUnit);
|
|
}
|
|
|
|
/**
|
|
* Consume a GoodsWish.
|
|
*
|
|
* @param aig The {@code AIGoods} to use.
|
|
* @param gw The {@code GoodsWish} to consume.
|
|
*/
|
|
private void consumeGoodsWish(AIGoods aig, GoodsWish gw) {
|
|
final Goods goods = aig.getGoods();
|
|
List<GoodsWish> gwL = goodsWishes.get(goods.getType());
|
|
gwL.remove(gw);
|
|
List<Wish> wl = transportDemand.get(gw.getDestination());
|
|
if (wl != null) wl.remove(gw);
|
|
gw.setTransportable(aig);
|
|
}
|
|
|
|
|
|
// Useful public routines
|
|
|
|
/**
|
|
* Gets the number of units that should build a colony.
|
|
*
|
|
* This is the desired total number, not the actual number which would
|
|
* take into account the number of existing BuildColonyMissions.
|
|
*
|
|
* @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, nColonySize1 = 0;
|
|
for (Settlement settlement : player.getSettlementList()) {
|
|
nColonies++;
|
|
if (settlement.isConnectedPort()) nPorts++;
|
|
final int colonySize = settlement.getUnitList().size();
|
|
if (colonySize == 1) {
|
|
nColonySize1++;
|
|
}
|
|
}
|
|
|
|
if (nPorts == 0) {
|
|
return 2;
|
|
}
|
|
if (nPorts == 1) {
|
|
return 1;
|
|
}
|
|
if (nColonies < 3) {
|
|
return 1;
|
|
}
|
|
if (nColonySize1 < 2) {
|
|
return 1;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
|
|
/**
|
|
* Asks the server to recruit a unit in Europe on behalf of the AIPlayer.
|
|
*
|
|
* FIXME: Move this to a specialized Handler class (AIEurope?)
|
|
* FIXME: Give protected access?
|
|
*
|
|
* @param slot The migration slot to recruit from.
|
|
* @return The new AIUnit created by this action or null on failure.
|
|
*/
|
|
private AIUnit recruitAIUnitInEurope(int slot) {
|
|
AIUnit aiUnit = null;
|
|
Europe europe = getPlayer().getEurope();
|
|
if (europe == null) return null;
|
|
int n = europe.getUnitCount();
|
|
final String selectAbility = Ability.SELECT_RECRUIT;
|
|
if (!Europe.MigrationType.validMigrantSlot(slot)) {
|
|
slot = (getPlayer().hasAbility(selectAbility))
|
|
? Europe.MigrationType.getDefaultSlot()
|
|
: Europe.MigrationType.getUnspecificSlot();
|
|
}
|
|
if (AIMessage.askEmigrate(this, slot)
|
|
&& europe.getUnitCount() == n+1) {
|
|
aiUnit = getAIUnit(europe.getUnitList().get(n));
|
|
if (aiUnit != null) addAIUnit(aiUnit);
|
|
}
|
|
return aiUnit;
|
|
}
|
|
|
|
/**
|
|
* Helper function for server communication - Ask the server
|
|
* to train a unit in Europe on behalf of the AIGetPlayer().
|
|
*
|
|
* FIXME: Move this to a specialized Handler class (AIEurope?)
|
|
* FIXME: Give protected access?
|
|
*
|
|
* @param unitType The {@code UnitType} to train.
|
|
* @return the new AIUnit created by this action. May be null.
|
|
*/
|
|
private AIUnit trainAIUnitInEurope(UnitType unitType) {
|
|
if (unitType == null) {
|
|
throw new RuntimeException("Invalid UnitType: " + this);
|
|
}
|
|
|
|
AIUnit aiUnit = null;
|
|
Europe europe = getPlayer().getEurope();
|
|
if (europe == null) return null;
|
|
int n = europe.getUnitCount();
|
|
|
|
if (AIMessage.askTrainUnitInEurope(this, unitType)
|
|
&& europe.getUnitCount() == n+1) {
|
|
aiUnit = getAIUnit(europe.getUnitList().get(n));
|
|
if (aiUnit != null) addAIUnit(aiUnit);
|
|
}
|
|
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() {
|
|
return sort(flatten(getAIColonies(), aic -> aic.getWishes().stream()),
|
|
ValuedAIObject.descendingValueComparator);
|
|
}
|
|
|
|
|
|
// Diplomacy support
|
|
|
|
/**
|
|
* Determines the stances towards each player.
|
|
*
|
|
* @param lb A {@code LogBuilder} to log to.
|
|
*/
|
|
private void determineStances(LogBuilder lb) {
|
|
final Player player = getPlayer();
|
|
lb.mark();
|
|
|
|
for (Player p : getGame().getLivePlayerList(player)) {
|
|
Stance newStance = determineStance(p);
|
|
if (newStance != player.getStance(p)) {
|
|
if (newStance == Stance.WAR && peaceHolds(p)) {
|
|
; // Peace treaty holds for now
|
|
} else {
|
|
getAIMain().getFreeColServer().getInGameController()
|
|
.changeStance(player, newStance, p, true);
|
|
lb.add(" ", p.getDebugName(), "->", newStance, ", ");
|
|
}
|
|
}
|
|
}
|
|
if (lb.grew("\n Stance changes:")) lb.shrink(", ");
|
|
}
|
|
|
|
/**
|
|
* See if a recent peace treaty still has force.
|
|
*
|
|
* @param p The {@code Player} to check for a peace treaty with.
|
|
* @return True if peace gets another chance.
|
|
*/
|
|
private boolean peaceHolds(Player p) {
|
|
final Player player = getPlayer();
|
|
final Turn turn = getGame().getTurn();
|
|
final double peaceProb = getSpecification()
|
|
.getPercentageMultiplier(GameOptions.PEACE_PROBABILITY);
|
|
|
|
int peaceTurn = -1;
|
|
for (HistoryEvent h : player.getHistory()) {
|
|
if (p.getId().equals(h.getPlayerId())
|
|
&& h.getTurn().getNumber() > peaceTurn) {
|
|
switch (h.getEventType()) {
|
|
case MAKE_PEACE: case FORM_ALLIANCE:
|
|
peaceTurn = h.getTurn().getNumber();
|
|
break;
|
|
case DECLARE_WAR:
|
|
peaceTurn = -1;
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if (peaceTurn < 0) return false;
|
|
|
|
int n = turn.getNumber() - peaceTurn;
|
|
float prob = (float)Math.pow(peaceProb, n);
|
|
// Apply Franklin's modifier
|
|
prob = p.apply(prob, turn, Modifier.PEACE_TREATY);
|
|
return prob > 0.0f
|
|
&& (randomInt(logger, "Peace holds?", getAIRandom(), 100)
|
|
< (int)(100.0f * prob));
|
|
}
|
|
|
|
|
|
/**
|
|
* Get a nation summary for another player.
|
|
*
|
|
* @param other The other {@code Player} to get the summary for.
|
|
* @return The current {@code NationSummary} for a player.
|
|
*/
|
|
protected NationSummary getNationSummary(Player other) {
|
|
final Player player = getPlayer();
|
|
NationSummary ns = player.getNationSummary(other);
|
|
if (ns != null) return ns;
|
|
AIMessage.askNationSummary(this, other);
|
|
return player.getNationSummary(other);
|
|
}
|
|
|
|
/**
|
|
* Get the land force strength ratio of this player with respect
|
|
* to another.
|
|
*
|
|
* @param other The other {@code Player}.
|
|
* @return The strength ratio (strength/sum(strengths)).
|
|
*/
|
|
protected double getStrengthRatio(Player other) {
|
|
return getPlayer().getStrengthRatio(other, false);
|
|
}
|
|
|
|
/**
|
|
* Is this player lagging in naval strength? Calculate the ratio
|
|
* of its naval strength to the average strength of other European
|
|
* colonial powers.
|
|
*
|
|
* @return The naval strength ratio, or negative if there are no other
|
|
* European colonial nations.
|
|
*/
|
|
protected double getNavalStrengthRatio() {
|
|
final Player player = getPlayer();
|
|
double navalAverage = 0.0;
|
|
double navalStrength = 0.0;
|
|
int nPlayers = 0;
|
|
for (Player p : transform(getGame().getLiveEuropeanPlayers(player),
|
|
x -> !x.isREF())) {
|
|
NationSummary ns = getNationSummary(p);
|
|
if (ns == null) continue;
|
|
if (p == player) {
|
|
navalStrength = ns.getNavalStrength();
|
|
} else {
|
|
int st = ns.getNavalStrength();
|
|
if (st >= 0) navalAverage += st;
|
|
nPlayers++;
|
|
}
|
|
}
|
|
if (nPlayers <= 0 || navalStrength < 0) return -1.0;
|
|
navalAverage /= nPlayers;
|
|
return (navalAverage == 0.0) ? -1.0 : navalStrength / navalAverage;
|
|
}
|
|
|
|
/**
|
|
* Reject a trade agreement, except if a Franklin-derived stance
|
|
* is supplied.
|
|
*
|
|
* @param stance A stance {@code TradeItem}.
|
|
* @param agreement The {@code DiplomaticTrade} to reset.
|
|
* @return The {@code TradeStatus} for the agreement.
|
|
*/
|
|
private TradeStatus rejectAgreement(TradeItem stance,
|
|
DiplomaticTrade agreement) {
|
|
if (stance == null) return TradeStatus.REJECT_TRADE;
|
|
|
|
agreement.clear();
|
|
agreement.add(stance);
|
|
return TradeStatus.PROPOSE_TRADE;
|
|
}
|
|
|
|
|
|
// Mission handling
|
|
|
|
/**
|
|
* Ensures all units have a mission.
|
|
*
|
|
* @param lb A {@code LogBuilder} to log to.
|
|
*/
|
|
protected void giveNormalMissions(LogBuilder lb, List<AIUnit> aiUnits) {
|
|
final AIMain aiMain = getAIMain();
|
|
final Player player = getPlayer();
|
|
BuildColonyMission bcm = null;
|
|
Mission m;
|
|
|
|
nBuilders = buildersNeeded();
|
|
nPioneers = pioneersNeeded();
|
|
nScouts = scoutsNeeded();
|
|
|
|
List<AIUnit> navalUnits = new ArrayList<>(aiUnits.size()/2);
|
|
List<AIUnit> done = new ArrayList<>(aiUnits.size());
|
|
List<TransportMission> transportMissions = new ArrayList<>(aiUnits.size()/2);
|
|
java.util.Map<Unit, String> reasons = new HashMap<>(aiUnits.size());
|
|
|
|
// 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. Accumulate potentially usable transport missions.
|
|
lb.mark();
|
|
for (AIUnit aiUnit : aiUnits) {
|
|
final Unit unit = aiUnit.getUnit();
|
|
final Colony colony = unit.getColony();
|
|
m = aiUnit.getMission();
|
|
final Location oldTarget = (m == null) ? null : m.getTarget();
|
|
|
|
if (!unit.isInitialized() || unit.isDisposed()) {
|
|
reasons.put(unit, "Invalid");
|
|
|
|
} else if (unit.isDamaged()) { // Damaged units must wait
|
|
if (!(m instanceof IdleAtSettlementMission)) {
|
|
if ((m = getIdleAtSettlementMission(aiUnit)) != null) {
|
|
lb.add(", ", m);
|
|
}
|
|
}
|
|
reasons.put(unit, "Damaged");
|
|
|
|
} else if (unit.getState() == UnitState.IN_COLONY
|
|
&& colony.getUnitCount() <= 1) {
|
|
// The unit has its hand full keeping the colony alive.
|
|
if (!(m instanceof WorkInsideColonyMission)
|
|
&& (m = getWorkInsideColonyMission(aiUnit,
|
|
aiMain.getAIColony(colony))) != null) {
|
|
logger.warning(aiUnit + " should WorkInsideColony at "
|
|
+ colony.getName());
|
|
lb.add(", ", m);
|
|
updateTransport(aiUnit, oldTarget, lb);
|
|
}
|
|
reasons.put(unit, "Vital");
|
|
|
|
} else if (unit.isInMission()) {
|
|
reasons.put(unit, "Mission");
|
|
|
|
} else if (m != null && m.isValid() && !m.isOneTime()) {
|
|
if (m instanceof BuildColonyMission) {
|
|
bcm = (BuildColonyMission)m;
|
|
nBuilders--;
|
|
} else if (m instanceof PioneeringMission) {
|
|
nPioneers--;
|
|
} else if (m instanceof ScoutingMission) {
|
|
nScouts--;
|
|
} else if (m instanceof TransportMission) {
|
|
TransportMission tm = (TransportMission)m;
|
|
// Consider reassigning quiescent transport
|
|
// missions to privateer missions
|
|
if (tm.isEmpty() && unit.isNaval()
|
|
&& unit.isOffensiveUnit()) {
|
|
navalUnits.add(aiUnit);
|
|
done.add(aiUnit);
|
|
continue;
|
|
}
|
|
// If there is capacity in this mission, consider adding
|
|
// more cargoes
|
|
if (tm.destinationCapacity() > 0) {
|
|
transportMissions.add(tm);
|
|
}
|
|
} else if (m instanceof PrivateerMission) {
|
|
if (!(m.getTarget() instanceof Unit)) {
|
|
// Privateering but not chasing a unit, consider
|
|
// reassigning to transport.
|
|
navalUnits.add(aiUnit);
|
|
done.add(aiUnit);
|
|
continue;
|
|
}
|
|
}
|
|
reasons.put(unit, "Valid");
|
|
|
|
} else if (unit.isNaval()) {
|
|
navalUnits.add(aiUnit);
|
|
|
|
} else if (unit.isAtSea()) { // Wait for it to emerge
|
|
reasons.put(unit, "At-Sea");
|
|
|
|
} else { // Needs mission
|
|
continue;
|
|
}
|
|
done.add(aiUnit);
|
|
}
|
|
aiUnits.removeAll(done);
|
|
done.clear();
|
|
|
|
// First try to satisfy the demand for missions with a defined
|
|
// quota. Builders first to keep weak players in the game,
|
|
// scouts next as they are profitable. Pile onto any existing
|
|
// building mission if there are no colonies.
|
|
if (bcm != null && !player.hasSettlements()) {
|
|
final Location bcmTarget = bcm.getTarget();
|
|
for (AIUnit aiUnit : sort(aiUnits, builderComparator)) {
|
|
final Location oldTarget = ((m = aiUnit.getMission()) == null)
|
|
? null : m.getTarget();
|
|
if ((m = getBuildColonyMission(aiUnit, bcmTarget)) == null)
|
|
continue;
|
|
lb.add(", ", m);
|
|
updateTransport(aiUnit, oldTarget, lb);
|
|
done.add(aiUnit);
|
|
if (requestsTransport(aiUnit)) transportSupply.add(aiUnit);
|
|
reasons.put(aiUnit.getUnit(), "0Builder");
|
|
}
|
|
aiUnits.removeAll(done);
|
|
done.clear();
|
|
}
|
|
if (nBuilders > 0) {
|
|
/*
|
|
* Temporary fix for endless amount of colonists not being assigned
|
|
* any mission. See BR#3322
|
|
*/
|
|
final int MAX_BUILDING_MISSION_TRIES = 50;
|
|
|
|
int tries = 0;
|
|
for (AIUnit aiUnit : sort(aiUnits, builderComparator)) {
|
|
if (aiUnit.getUnit().isArmed() && getGame().getTurn().getNumber() > 20) {
|
|
// Quickfix to avoid having all soldies being given a BuildColonyMission.
|
|
continue;
|
|
}
|
|
tries++;
|
|
if (tries > MAX_BUILDING_MISSION_TRIES) {
|
|
break;
|
|
}
|
|
final Location oldTarget = ((m = aiUnit.getMission()) == null)
|
|
? null : m.getTarget();
|
|
if ((m = getBuildColonyMission(aiUnit, null)) == null)
|
|
continue;
|
|
lb.add(", ", m);
|
|
updateTransport(aiUnit, oldTarget, lb);
|
|
done.add(aiUnit);
|
|
if (requestsTransport(aiUnit)) transportSupply.add(aiUnit);
|
|
reasons.put(aiUnit.getUnit(), "Builder" + nBuilders);
|
|
if (--nBuilders <= 0) break;
|
|
}
|
|
aiUnits.removeAll(done);
|
|
done.clear();
|
|
}
|
|
if (nScouts > 0) {
|
|
for (AIUnit aiUnit : sort(aiUnits, scoutComparator)) {
|
|
final Location oldTarget = ((m = aiUnit.getMission()) == null)
|
|
? null : m.getTarget();
|
|
final Unit unit = aiUnit.getUnit();
|
|
if ((m = getScoutingMission(aiUnit)) == null) continue;
|
|
lb.add(", ", m);
|
|
updateTransport(aiUnit, oldTarget, lb);
|
|
done.add(aiUnit);
|
|
if (requestsTransport(aiUnit)) transportSupply.add(aiUnit);
|
|
reasons.put(unit, "Scout" + nScouts);
|
|
if (--nScouts <= 0) break;
|
|
}
|
|
aiUnits.removeAll(done);
|
|
done.clear();
|
|
}
|
|
if (nPioneers > 0) {
|
|
for (AIUnit aiUnit : sort(aiUnits, pioneerComparator)) {
|
|
final Unit unit = aiUnit.getUnit();
|
|
final Location oldTarget = ((m = aiUnit.getMission()) == null)
|
|
? null : m.getTarget();
|
|
if ((m = getPioneeringMission(aiUnit, null)) == null) continue;
|
|
lb.add(", ", m);
|
|
updateTransport(aiUnit, oldTarget, lb);
|
|
done.add(aiUnit);
|
|
if (requestsTransport(aiUnit)) transportSupply.add(aiUnit);
|
|
reasons.put(unit, "Pioneer" + nPioneers);
|
|
if (--nPioneers <= 0) break;
|
|
}
|
|
aiUnits.removeAll(done);
|
|
done.clear();
|
|
}
|
|
|
|
// Give the remaining land units a valid mission.
|
|
for (AIUnit aiUnit : aiUnits) {
|
|
final Unit unit = aiUnit.getUnit();
|
|
final Location oldTarget = ((m = aiUnit.getMission()) == null)
|
|
? null : m.getTarget();
|
|
if ((m = getSimpleMission(aiUnit)) == null) continue;
|
|
lb.add(", ", m);
|
|
updateTransport(aiUnit, oldTarget, lb);
|
|
reasons.put(unit, "New-Land");
|
|
done.add(aiUnit);
|
|
if (requestsTransport(aiUnit)) transportSupply.add(aiUnit);
|
|
}
|
|
aiUnits.removeAll(done);
|
|
done.clear();
|
|
|
|
// Process the free naval units, possibly adding to the usable
|
|
// transport missions.
|
|
for (AIUnit aiUnit : navalUnits) {
|
|
final Unit unit = aiUnit.getUnit();
|
|
Mission old = ((m = aiUnit.getMission()) != null && m.isValid())
|
|
? m : null;
|
|
if ((m = getSimpleMission(aiUnit)) == null) continue;
|
|
lb.add(", ", m, ((m == old) ? " (preserved)" : " (new)"));
|
|
reasons.put(unit, "New-Naval");
|
|
done.add(aiUnit);
|
|
if (m instanceof TransportMission) {
|
|
TransportMission tm = (TransportMission)m;
|
|
if (tm.destinationCapacity() > 0) {
|
|
transportMissions.add(tm);
|
|
}
|
|
// A new transport mission might have retargeted
|
|
// its passengers into new valid missions.
|
|
for (Unit u : aiUnit.getUnit().getUnitList()) {
|
|
AIUnit aiu = getAIUnit(u);
|
|
Mission um = aiu.getMission();
|
|
if (um != null && um.isValid()
|
|
&& aiUnits.contains(aiu)) {
|
|
aiUnits.remove(aiu);
|
|
reasons.put(aiu.getUnit(), "TNew");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
navalUnits.removeAll(done);
|
|
done.clear();
|
|
|
|
// Give remaining units the fallback mission.
|
|
aiUnits.addAll(navalUnits);
|
|
List<Colony> ports = null;
|
|
int nPorts = player.getNumberOfPorts();
|
|
for (AIUnit aiUnit : aiUnits) {
|
|
final Unit unit = aiUnit.getUnit();
|
|
m = aiUnit.getMission();
|
|
final Location oldTarget = (m == null) ? null : m.getTarget();
|
|
if (m != null && m.isValid() && !m.isOneTime()) {
|
|
logger.warning("Trying fallback mission for unit " + unit
|
|
+ " with valid mission " + m
|
|
+ " reason " + reasons.get(unit));
|
|
continue;
|
|
}
|
|
|
|
if (nPorts > 0 && unit.isInEurope() && unit.isPerson()) {
|
|
// Choose a port to add to
|
|
if (ports == null) ports = player.getConnectedPortList();
|
|
Colony c = ports.remove(0);
|
|
AIColony aic = aiMain.getAIColony(c);
|
|
if ((m = getWorkInsideColonyMission(aiUnit, aic)) != null) {
|
|
lb.add(", ", m);
|
|
updateTransport(aiUnit, oldTarget, lb);
|
|
reasons.put(unit, "To-work");
|
|
}
|
|
ports.add(c);
|
|
} else if (m instanceof IdleAtSettlementMission) {
|
|
reasons.put(unit, "Idle"); // already idle
|
|
} else {
|
|
if ((m = getIdleAtSettlementMission(aiUnit)) != null) {
|
|
lb.add(", ", m);
|
|
updateTransport(aiUnit, oldTarget, lb);
|
|
reasons.put(unit, "Idle");
|
|
}
|
|
}
|
|
}
|
|
lb.grew("\n Mission changes");
|
|
|
|
// Now see if transport can be found
|
|
allocateTransportables(getUrgentTransportables(),
|
|
transportMissions, lb);
|
|
|
|
// Log
|
|
if (!aiUnits.isEmpty()) {
|
|
lb.add("\n Free Land Units:");
|
|
for (AIUnit aiu : aiUnits) {
|
|
lb.add(" ", aiu.getUnit());
|
|
}
|
|
}
|
|
if (!navalUnits.isEmpty()) {
|
|
lb.add("\n Free Naval Units:");
|
|
for (AIUnit aiu : navalUnits) {
|
|
lb.add(" ", aiu.getUnit());
|
|
}
|
|
}
|
|
lb.add("\n Missions(colonies=", player.getSettlementCount(),
|
|
" builders=", nBuilders,
|
|
" pioneers=", nPioneers,
|
|
" scouts=", nScouts,
|
|
" naval-carriers=", nNavalCarrier,
|
|
")");
|
|
logMissions(reasons, lb);
|
|
}
|
|
|
|
/**
|
|
* Choose a mission for an AIUnit.
|
|
*
|
|
* @param aiUnit The {@code AIUnit} to choose for.
|
|
* @return A suitable {@code Mission}, or null if none found.
|
|
*/
|
|
private Mission getSimpleMission(AIUnit aiUnit) {
|
|
final Unit unit = aiUnit.getUnit();
|
|
Mission m, ret;
|
|
final Mission old = ((m = aiUnit.getMission()) != null && m.isValid())
|
|
? m : null;
|
|
|
|
if (unit.isNaval()) {
|
|
ret = (old instanceof PrivateerMission) ? old
|
|
: (!unit.isInEurope() && (m = getPrivateerMission(aiUnit, null)) != null) ? m
|
|
: (old instanceof TransportMission) ? old
|
|
: ((m = getTransportMission(aiUnit)) != null) ? m
|
|
: ((m = getPrivateerMission(aiUnit, null)) != null) ? m
|
|
: (old instanceof UnitSeekAndDestroyMission) ? old
|
|
: ((m = getSeekAndDestroyMission(aiUnit, 8)) != null) ? m
|
|
: (old instanceof UnitWanderHostileMission) ? old
|
|
: getWanderHostileMission(aiUnit);
|
|
|
|
} else if (unit.isCarrier()) {
|
|
ret = getTransportMission(aiUnit);
|
|
|
|
} else {
|
|
// CashIn missions are obvious
|
|
ret = (old instanceof CashInTreasureTrainMission) ? old
|
|
: ((m = getCashInTreasureTrainMission(aiUnit)) != null) ? m
|
|
|
|
// Working in colony is obvious
|
|
: (unit.isInColony()
|
|
&& old instanceof WorkInsideColonyMission) ? old
|
|
: (unit.isInColony()
|
|
&& (m = getWorkInsideColonyMission(aiUnit, null)) != null) ? m
|
|
|
|
// Try to maintain local defence
|
|
: (old instanceof DefendSettlementMission && old.getTarget() instanceof Colony && !((Colony) old.getTarget()).isVeryWellDefended()) ? old
|
|
: ((m = getDefendCurrentSettlementMission(aiUnit)) != null) ? m
|
|
|
|
// REF override
|
|
: (unit.hasAbility(Ability.REF_UNIT))
|
|
? ((old instanceof UnitSeekAndDestroyMission) ? old
|
|
: ((m = getSeekAndDestroyMission(aiUnit, 12)) != null) ? m
|
|
: (m = getWanderHostileMission(aiUnit)))
|
|
|
|
// Favour wish realization for expert units
|
|
: (unit.isColonist() && unit.getSkillLevel() > 0
|
|
&& old instanceof WishRealizationMission) ? old
|
|
: (unit.isColonist() && unit.getSkillLevel() > 0
|
|
&& (m = getWishRealizationMission(aiUnit, null)) != null) ? m
|
|
|
|
// Ordinary defence
|
|
: ((m = getDefendSettlementMission(aiUnit, false, false)) != null) ? m
|
|
|
|
// Try nearby offence
|
|
: (old instanceof UnitSeekAndDestroyMission) ? old
|
|
: ((m = getSeekAndDestroyMission(aiUnit, 8)) != null) ? m
|
|
|
|
// Missionary missions are only available to some units
|
|
: (old instanceof MissionaryMission) ? old
|
|
: ((m = getMissionaryMission(aiUnit)) != null) ? m
|
|
|
|
// Try to satisfy any remaining wishes, such as population
|
|
: (old instanceof WishRealizationMission) ? old
|
|
: ((m = getWishRealizationMission(aiUnit, null)) != null) ? m
|
|
|
|
// Another try to defend, with relaxed cost decider
|
|
: ((m = getDefendSettlementMission(aiUnit, true, false)) != null) ? m
|
|
|
|
// Another try to attack, at longer range
|
|
: ((m = getSeekAndDestroyMission(aiUnit, 16)) != null) ? m
|
|
|
|
// Try again, even for well defended colonies.
|
|
: ((m = getDefendSettlementMission(aiUnit, true, true)) != null) ? m
|
|
|
|
// Leftover offensive units should go out looking for trouble
|
|
: (old instanceof UnitWanderHostileMission) ? old
|
|
: ((m = getWanderHostileMission(aiUnit)) != null) ? m
|
|
|
|
: null;
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
// Mission creation convenience routines.
|
|
// Aggregated here for uniformity. Might have been more logical
|
|
// to disperse them to the individual classes.
|
|
|
|
/**
|
|
* Gets a new BuildColonyMission for a unit.
|
|
*
|
|
* @param aiUnit The {@code AIUnit} to check.
|
|
* @param target An optional target {@code Location}.
|
|
* @return A new mission, or null if impossible.
|
|
*/
|
|
private Mission getBuildColonyMission(AIUnit aiUnit, Location target) {
|
|
String reason = BuildColonyMission.invalidMissionReason(aiUnit);
|
|
if (reason != null) return null;
|
|
final Unit unit = aiUnit.getUnit();
|
|
if (target == null) {
|
|
target = BuildColonyMission.findMissionTarget(aiUnit,
|
|
buildingRange, unit.isInEurope());
|
|
}
|
|
return (target == null) ? null
|
|
: new BuildColonyMission(getAIMain(), aiUnit, target);
|
|
}
|
|
|
|
/**
|
|
* Gets a new CashInTreasureTrainMission for a unit.
|
|
*
|
|
* @param aiUnit The {@code AIUnit} to check.
|
|
* @return A new mission, or null if impossible.
|
|
*/
|
|
private Mission getCashInTreasureTrainMission(AIUnit aiUnit) {
|
|
String reason = CashInTreasureTrainMission.invalidMissionReason(aiUnit);
|
|
if (reason != null) return null;
|
|
final Unit unit = aiUnit.getUnit();
|
|
Location loc = CashInTreasureTrainMission.findMissionTarget(aiUnit,
|
|
cashInRange, unit.isInEurope());
|
|
return (loc == null) ? null
|
|
: new CashInTreasureTrainMission(getAIMain(), aiUnit, loc);
|
|
}
|
|
|
|
/**
|
|
* Gets a new DefendSettlementMission for a unit.
|
|
*
|
|
* @param aiUnit The {@code AIUnit} to check.
|
|
* @param relaxed Use a relaxed cost decider to choose the target.
|
|
* @param includeWellDefendedSettlements If {@code true}, then colonies that
|
|
* are already well defended can get a DefendSettlementMission.
|
|
* @return A new mission, or null if impossible.
|
|
*/
|
|
public Mission getDefendSettlementMission(AIUnit aiUnit, boolean relaxed, boolean includeWellDefendedSettlements) {
|
|
if (DefendSettlementMission.invalidMissionReason(aiUnit) != null) return null;
|
|
final Unit unit = aiUnit.getUnit();
|
|
final Location loc = unit.getLocation();
|
|
double worstValue = Double.MAX_VALUE;
|
|
Colony worstColony = null;
|
|
for (AIColony aic : getAIColonies()) {
|
|
Colony colony = aic.getColony();
|
|
if (aic.isBadlyDefended() || includeWellDefendedSettlements) {
|
|
if (unit.isAtLocation(colony.getTile())) {
|
|
worstColony = colony;
|
|
break;
|
|
}
|
|
int ttr = 1 + unit.getTurnsToReach(loc, colony.getTile(),
|
|
unit.getCarrier(),
|
|
((relaxed) ? CostDeciders.numberOfTiles() : null));
|
|
if (ttr >= Unit.MANY_TURNS) continue;
|
|
double value = colony.getDefenceRatio() * 10 + ttr;
|
|
if (value < worstValue) {
|
|
worstValue = value;
|
|
worstColony = colony;
|
|
}
|
|
}
|
|
}
|
|
return (worstColony == null) ? null
|
|
: getDefendSettlementMission(aiUnit, worstColony);
|
|
}
|
|
|
|
/**
|
|
* Gets a new MissionaryMission for a unit.
|
|
*
|
|
* @param aiUnit The {@code AIUnit} to check.
|
|
* @return A new mission, or null if impossible.
|
|
*/
|
|
public Mission getMissionaryMission(AIUnit aiUnit) {
|
|
if (MissionaryMission.prepare(aiUnit) != null) return null;
|
|
Location loc = MissionaryMission.findMissionTarget(aiUnit,
|
|
missionaryRange, true);
|
|
if (loc == null) {
|
|
aiUnit.equipForRole(getSpecification().getDefaultRole());
|
|
return null;
|
|
}
|
|
return new MissionaryMission(getAIMain(), aiUnit, loc);
|
|
}
|
|
|
|
/**
|
|
* Gets a new PioneeringMission for a unit.
|
|
*
|
|
* FIXME: pioneers to make roads between colonies
|
|
*
|
|
* @param aiUnit The {@code AIUnit} to check.
|
|
* @param target An optional target {@code Location}.
|
|
* @return A new mission, or null if impossible.
|
|
*/
|
|
public Mission getPioneeringMission(AIUnit aiUnit, Location target) {
|
|
if (PioneeringMission.prepare(aiUnit) != null) return null;
|
|
if (target == null) {
|
|
target = PioneeringMission.findMissionTarget(aiUnit,
|
|
pioneeringRange, true);
|
|
}
|
|
if (target == null) {
|
|
Unit unit = aiUnit.getUnit();
|
|
if (unit.isInEurope() || unit.getSettlement() != null) {
|
|
aiUnit.equipForRole(getSpecification().getDefaultRole());
|
|
}
|
|
return null;
|
|
}
|
|
return new PioneeringMission(getAIMain(), aiUnit, target);
|
|
}
|
|
|
|
/**
|
|
* Gets a new PrivateerMission for a unit.
|
|
*
|
|
* @param aiUnit The {@code AIUnit} to check.
|
|
* @param target An optional target {@code Location}.
|
|
* @return A new mission, or null if impossible.
|
|
*/
|
|
public Mission getPrivateerMission(AIUnit aiUnit, Location target) {
|
|
if (PrivateerMission.invalidMissionReason(aiUnit) != null) return null;
|
|
if (target == null) {
|
|
target = PrivateerMission.findMissionTarget(aiUnit, privateerRange, true);
|
|
}
|
|
return (target == null) ? null
|
|
: new PrivateerMission(getAIMain(), aiUnit, target);
|
|
}
|
|
|
|
/**
|
|
* Gets a new ScoutingMission for a unit.
|
|
*
|
|
* @param aiUnit The {@code AIUnit} to check.
|
|
* @return A new mission, or null if impossible.
|
|
*/
|
|
public Mission getScoutingMission(AIUnit aiUnit) {
|
|
if (ScoutingMission.prepare(aiUnit) != null) return null;
|
|
Location loc = ScoutingMission.findMissionTarget(aiUnit,
|
|
scoutingRange, true);
|
|
if (loc == null) {
|
|
Unit unit = aiUnit.getUnit();
|
|
if (unit.isInEurope() || unit.getSettlement() != null) {
|
|
aiUnit.equipForRole(getSpecification().getDefaultRole());
|
|
}
|
|
return null;
|
|
}
|
|
return new ScoutingMission(getAIMain(), aiUnit, loc);
|
|
}
|
|
|
|
/**
|
|
* Gets a new TransportMission for a unit.
|
|
*
|
|
* @param aiUnit The {@code AIUnit} to check.
|
|
* @return A new mission, or null if impossible.
|
|
*/
|
|
public Mission getTransportMission(AIUnit aiUnit) {
|
|
if (TransportMission.invalidMissionReason(aiUnit) != null) return null;
|
|
return new TransportMission(getAIMain(), aiUnit);
|
|
}
|
|
|
|
/**
|
|
* Gets a new WishRealizationMission for a unit.
|
|
*
|
|
* @param aiUnit The {@code AIUnit} to check.
|
|
* @param wish An optional {@code WorkerWish} to realize.
|
|
* @return A new mission, or null if impossible.
|
|
*/
|
|
private Mission getWishRealizationMission(AIUnit aiUnit, WorkerWish wish) {
|
|
if (WishRealizationMission.invalidMissionReason(aiUnit) != null) return null;
|
|
final Unit unit = aiUnit.getUnit();
|
|
if (wish == null) {
|
|
wish = getBestWorkerWish(aiUnit, unit.getType());
|
|
}
|
|
if (wish == null) return null;
|
|
consumeWorkerWish(aiUnit, wish);
|
|
return new WishRealizationMission(getAIMain(), aiUnit, wish);
|
|
}
|
|
|
|
/**
|
|
* Gets a WorkInsideColonyMission for a unit.
|
|
*
|
|
* @param aiUnit The {@code AIUnit} to check.
|
|
* @param aiColony An optional {@code AIColony} to work at.
|
|
* @return A new mission, or null if impossible.
|
|
*/
|
|
public Mission getWorkInsideColonyMission(AIUnit aiUnit,
|
|
AIColony aiColony) {
|
|
if (aiColony == null) {
|
|
aiColony = getAIColony(aiUnit.getUnit().getColony());
|
|
}
|
|
if (WorkInsideColonyMission.invalidMissionReason(aiUnit, aiColony.getColony()) != null) {
|
|
return null;
|
|
}
|
|
return (aiColony == null) ? null
|
|
: new WorkInsideColonyMission(getAIMain(), aiUnit, aiColony);
|
|
}
|
|
|
|
|
|
// AIPlayer interface
|
|
|
|
/**
|
|
* {@inheritDoc}
|
|
*/
|
|
@Override
|
|
protected Stance determineStance(Player other) {
|
|
final Player player = getPlayer();
|
|
return (other.isREF())
|
|
? ((player.getREFPlayer() == other)
|
|
// At war with our REF if rebel, otherwise at peace.
|
|
? ((player.isRebel()) ? Stance.WAR : Stance.PEACE)
|
|
// Do not mess with other player's REF unless they conquer
|
|
// their rebellious colonies.
|
|
: ((!other.getRebels().isEmpty()) ? Stance.PEACE
|
|
: super.determineStance(other)))
|
|
// Use normal stance determination for non-REF nations.
|
|
: super.determineStance(other);
|
|
}
|
|
|
|
/**
|
|
* {@inheritDoc}
|
|
*/
|
|
@Override
|
|
public void startWorking() {
|
|
final Player player = getPlayer();
|
|
final Turn turn = getGame().getTurn();
|
|
final Specification spec = getSpecification();
|
|
initializeFromSpecification(spec);
|
|
|
|
// This is happening, very rarely. Hopefully now fixed by
|
|
// synchronizing access to AIMain.aiObjects.
|
|
if (getAIMain().getAIPlayer(player) != this) {
|
|
throw new RuntimeException("EuropeanAIPlayer integrity fail: " + player);
|
|
}
|
|
clearAIUnits();
|
|
player.clearNationCache();
|
|
badlyDefended.clear();
|
|
|
|
// Note call to getAIUnits(). This triggers
|
|
// AIPlayer.createAIUnits which we want to do early, certainly
|
|
// before cheat() or other operations that might make new units
|
|
// happen.
|
|
LogBuilder lb = new LogBuilder(1024);
|
|
int colonyCount = getAIColonies().size();
|
|
lb.add(player.getDebugName(),
|
|
" in ", turn, "/", turn.getNumber(),
|
|
" units=", getAIUnits().size(),
|
|
" colonies=", colonyCount,
|
|
" declare=", (player.checkDeclareIndependence() == null),
|
|
" v-land-REF=", player.getRebelStrengthRatio(false),
|
|
" v-naval-REF=", player.getRebelStrengthRatio(true));
|
|
if (turn.isFirstTurn()) initializeMissions(lb);
|
|
|
|
if (isLikesAttackingNatives() && getGame().getTurn().getNumber() > 100) {
|
|
for (Player p : getGame().getLivePlayerList(player)) {
|
|
if (!p.isIndian()) {
|
|
continue;
|
|
}
|
|
player.getTension(p).setValue(Tension.TENSION_MAX);
|
|
}
|
|
}
|
|
|
|
determineStances(lb);
|
|
|
|
if (colonyCount > 0) {
|
|
lb.add("\n Badly defended:"); // FIXME: prioritize defence
|
|
for (AIColony aic : getAIColonies()) {
|
|
if (aic.isBadlyDefended()) {
|
|
badlyDefended.add(aic);
|
|
lb.add(" ", aic.getColony());
|
|
}
|
|
}
|
|
|
|
lb.add("\n Update colonies:");
|
|
for (AIColony aic : getAIColonies()) aic.update(lb);
|
|
|
|
buildTipMap(lb);
|
|
buildWishMaps(lb);
|
|
}
|
|
cheat(lb);
|
|
buyUnitsInEurope(lb);
|
|
|
|
// Note order of operations below. We allow rearrange et al to run
|
|
// even when there are no movable units left because this expedites
|
|
// mission assignment.
|
|
List<AIUnit> aiUnits = getAIUnits();
|
|
final Set<AIUnit> militaryUnits = getAIUnits().stream()
|
|
.filter(MilitaryCoordinator.isUnitHandledByMilitaryCoordinator())
|
|
.collect(Collectors.toSet());
|
|
|
|
final MilitaryCoordinator militaryCoordinator = new MilitaryCoordinator(this, militaryUnits);
|
|
militaryCoordinator.determineMissions();
|
|
|
|
buildTransportMaps(lb);
|
|
|
|
final List<AIUnit> normalAiUnits = getAIUnits().stream()
|
|
.filter(MilitaryCoordinator.isUnitHandledByMilitaryCoordinator().negate())
|
|
.collect(Collectors.toList());
|
|
for (int i = 0; i < 3; i++) {
|
|
rearrangeColonies(lb);
|
|
giveNormalMissions(lb, normalAiUnits);
|
|
bringGifts(lb);
|
|
demandTribute(lb);
|
|
if (aiUnits.isEmpty()) break;
|
|
aiUnits = doMissions(aiUnits, lb);
|
|
}
|
|
|
|
|
|
lb.log(logger, Level.FINE);
|
|
|
|
clearAIUnits();
|
|
tipMap.clear();
|
|
transportDemand.clear();
|
|
transportSupply.clear();
|
|
wagonsNeeded.clear();
|
|
goodsWishes.clear();
|
|
workerWishes.clear();
|
|
}
|
|
|
|
private void buyUnitsInEurope(LogBuilder lb) {
|
|
/*
|
|
* It seems that training/recruiting units, in other cases than cheating,
|
|
* was removed from the code in 2012. This prevents the AI from actually
|
|
* using the money it's gaining. This happened in commit:
|
|
* 9e68ade8d2876c8135524c5b396c93d6c4d5ed1f.
|
|
*
|
|
* The code has changed a lot since then, so I have just added the quickfix
|
|
* below for buying units. This code does not prioritize wishes based on
|
|
* multiple units going to the same location, does not support recruiting etc.
|
|
*
|
|
* A better implementation will be added some point in the future.
|
|
*/
|
|
|
|
final Player player = getPlayer();
|
|
final Europe europe = player.getEurope();
|
|
|
|
if (player.getEurope() == null) {
|
|
return;
|
|
}
|
|
|
|
final long numberOfUnitsInDock = player.getEurope().getUnits().filter(unit -> !unit.isNaval()).count();
|
|
final long numberOfShips = getAIUnits().stream().filter(au -> au.getUnit().isNaval()).count();
|
|
|
|
if (player.getEurope() != null
|
|
&& numberOfUnitsInDock > 6
|
|
&& numberOfShips < 15) {
|
|
if (!buyShip()) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (numberOfUnitsInDock > 30) {
|
|
return;
|
|
}
|
|
|
|
boolean militaryUnitBought = numberOfUnitsInDock >= 18;
|
|
if (!militaryUnitBought) {
|
|
if (reallyNeedsMoreArtillery() || isLikesAttackingNatives() && needsMoreArtillery()) {
|
|
final Unit unitBought = buyArtillery();
|
|
if (unitBought == null) {
|
|
return;
|
|
}
|
|
militaryUnitBought = true;
|
|
}
|
|
if (reallyNeedsMoreDragoons() || isLikesAttackingNatives() && needsMoreDragoons()) {
|
|
final Unit unitBought = buyDragoon();
|
|
if (unitBought == null) {
|
|
return;
|
|
}
|
|
militaryUnitBought = true;
|
|
}
|
|
}
|
|
|
|
for (Wish w : getWishes()) {
|
|
if (!(w instanceof WorkerWish)) {
|
|
continue;
|
|
}
|
|
if (w.getTransportable() != null) {
|
|
continue;
|
|
}
|
|
|
|
final WorkerWish workerWish = (WorkerWish) w;
|
|
final UnitType unitType = workerWish.getUnitType();
|
|
if (!unitType.isAvailableTo(player) ) {
|
|
continue;
|
|
}
|
|
|
|
final int unitPrice = europe.getUnitPrice(unitType);
|
|
|
|
if (unitPrice <= 0) {
|
|
continue;
|
|
}
|
|
|
|
if (unitPrice > player.getGold()) {
|
|
return;
|
|
}
|
|
|
|
final AIUnit newUnit = trainAIUnitInEurope(unitType);
|
|
if (newUnit != null) {
|
|
getWishRealizationMission(newUnit, workerWish);
|
|
}
|
|
|
|
if (!militaryUnitBought) {
|
|
militaryUnitBought = true;
|
|
final Unit unitBought = buyDragoon();
|
|
if (unitBought == null) {
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
for (int i=0; i<6; i++) {
|
|
final Unit unitBought = buyDragoon();
|
|
if (unitBought == null) {
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
private boolean buyShip() {
|
|
final List<UnitType> unitTypes = new ArrayList<>(transform(getSpecification().getUnitTypeList(),
|
|
ut -> ut.hasAbility(Ability.NAVAL_UNIT)
|
|
&& ut.isAvailableTo(getPlayer())
|
|
&& ut.hasPrice()
|
|
&& ut.getSpace() > 0));
|
|
Collections.shuffle(unitTypes);
|
|
if (unitTypes.isEmpty()) {
|
|
return false;
|
|
}
|
|
|
|
final AbstractUnit au = new AbstractUnit(unitTypes.get(0), Specification.DEFAULT_ROLE_ID, 1);
|
|
final int purchasePrice = getPlayer().getEuropeanPurchasePrice(au);
|
|
if (purchasePrice <= 0 || purchasePrice == INFINITY) {
|
|
return false;
|
|
}
|
|
if (purchasePrice > getPlayer().getGold()) {
|
|
return false;
|
|
}
|
|
|
|
getPlayer().modifyGold(-purchasePrice);
|
|
final List<Unit> createdUnit = ((ServerPlayer) getPlayer()).createUnits(List.of(au), getPlayer().getEurope(), getAIRandom());
|
|
return true;
|
|
}
|
|
|
|
private boolean needsMoreDragoons() {
|
|
return 2 * (getAIColonies().size() + 1) > getAIUnits().stream().filter(au -> au.getUnit().isMounted() && au.getUnit().isArmed()).count();
|
|
}
|
|
|
|
private boolean reallyNeedsMoreDragoons() {
|
|
return getAIColonies().size() + 1 > getAIUnits().stream().filter(au -> au.getUnit().isMounted() && au.getUnit().isArmed()).count();
|
|
}
|
|
|
|
public boolean reallyNeedsMoreArtillery() {
|
|
return getAIColonies().size() / 2 + 1 > getAIUnits().stream().filter(au -> au.getUnit().hasAbility(Ability.BOMBARD)).count();
|
|
}
|
|
|
|
public boolean needsMoreArtillery() {
|
|
return getAIColonies().size() + 1 > getAIUnits().stream().filter(au -> au.getUnit().hasAbility(Ability.BOMBARD)).count();
|
|
}
|
|
|
|
private Unit buyDragoon() {
|
|
final Player player = getPlayer();
|
|
final Role dragoonRole = getSpecification().getMilitaryRolesList().stream()
|
|
.filter(r -> r.hasAbility(Ability.ARMED) && r.hasAbility(Ability.MOUNTED))
|
|
.findFirst()
|
|
.orElse(null);
|
|
|
|
if (dragoonRole == null) {
|
|
return null;
|
|
}
|
|
|
|
final UnitType cheapestUnitType = getSpecification().getUnitTypesTrainedInEurope(getPlayer())
|
|
.stream()
|
|
.filter(ut -> ut.getPrice() > 0 && ut.getPrice() != INFINITY)
|
|
.sorted((a, b) -> Integer.compare(a.getPrice(), b.getPrice()))
|
|
.findFirst()
|
|
.orElse(null);
|
|
|
|
if (cheapestUnitType == null) {
|
|
return null;
|
|
}
|
|
|
|
final AbstractUnit au = new AbstractUnit(cheapestUnitType, dragoonRole.getId(), 1);
|
|
final int purchasePrice = player.getEuropeanPurchasePrice(au);
|
|
if (purchasePrice <= 0 || purchasePrice == INFINITY) {
|
|
return null;
|
|
}
|
|
if (purchasePrice > getPlayer().getGold()) {
|
|
return null;
|
|
}
|
|
|
|
player.modifyGold(-purchasePrice);
|
|
|
|
final AbstractUnit auForCreation = new AbstractUnit(getSpecification().getDefaultUnitType(),
|
|
dragoonRole.getId(), 1);
|
|
final List<Unit> createdUnit = ((ServerPlayer) player).createUnits(List.of(auForCreation), player.getEurope(), getAIRandom());
|
|
if (createdUnit.isEmpty()) {
|
|
return null;
|
|
}
|
|
return createdUnit.get(0);
|
|
}
|
|
|
|
private Unit buyArtillery() {
|
|
final Player player = getPlayer();
|
|
final Europe europe = player.getEurope();
|
|
final UnitType cheapestArtilleryUnitType = getSpecification().getUnitTypesPurchasedInEurope(getPlayer())
|
|
.stream()
|
|
.filter(ut -> ut.getPrice() > 0 && ut.getPrice() != INFINITY)
|
|
.filter(ut -> ut.hasAbility(Ability.BOMBARD))
|
|
.filter(ut -> ut.isAvailableTo(player))
|
|
.sorted((a, b) -> Integer.compare(a.getPrice(), b.getPrice()))
|
|
.findFirst()
|
|
.orElse(null);
|
|
|
|
if (cheapestArtilleryUnitType == null) {
|
|
return null;
|
|
}
|
|
|
|
final int unitPrice = europe.getUnitPrice(cheapestArtilleryUnitType);
|
|
if (unitPrice > player.getGold()) {
|
|
return null;
|
|
}
|
|
|
|
final AIUnit newUnit = trainAIUnitInEurope(cheapestArtilleryUnitType);
|
|
if (newUnit == null) {
|
|
return null;
|
|
}
|
|
|
|
return newUnit.getUnit();
|
|
}
|
|
|
|
/**
|
|
* {@inheritDoc}
|
|
*/
|
|
@Override
|
|
protected List<AIUnit> doMissions(List<AIUnit> aiUnits, LogBuilder lb) {
|
|
lb.add("\n Do missions:");
|
|
List<AIUnit> result = new ArrayList<>();
|
|
|
|
// For all units, do their mission and collect the ones that need
|
|
// to be revisited.
|
|
for (AIUnit aiu : aiUnits) {
|
|
final Unit unit = aiu.getUnit();
|
|
if (unit == null || unit.isDisposed()) continue;
|
|
|
|
// giveNormalMissions should have given all units a
|
|
// mission, but TransportMissions may have delivered a
|
|
// unit and completed its WishRealizationMission, so it is
|
|
// possible for a null mission to happen here. Refer such
|
|
// units back to giveNormalMissions.
|
|
final Mission oldMission = aiu.getMission();
|
|
if (oldMission == null) {
|
|
result.add(aiu);
|
|
continue;
|
|
}
|
|
final Location oldTarget = oldMission.getTarget();
|
|
final Location oldLocation = unit.getLocation();
|
|
final Colony oldColony = oldLocation.getColony();
|
|
|
|
// Do the mission. Clean up dead units.
|
|
lb.add("\n ", unit, " ");
|
|
try {
|
|
aiu.doMission(lb);
|
|
} catch (Exception e) {
|
|
lb.add(", EXCEPTION: ", e.getMessage());
|
|
logger.log(Level.WARNING, "doMissions failed for: " + aiu, e);
|
|
}
|
|
if (unit.isDisposed() || unit.getLocation() == null) {
|
|
aiu.dropTransport();
|
|
lb.add(", DIED.");
|
|
continue;
|
|
}
|
|
|
|
updateTransport(aiu, oldTarget, lb);
|
|
// Check again that the unit is alive, updateTransport() can
|
|
// cause unit to disembark onto a fatal LCR!
|
|
if (unit.isDisposed() || unit.getLocation() == null) {
|
|
lb.add(", DIED.");
|
|
continue;
|
|
}
|
|
|
|
// Units with moves left should be requeued. If they are on a
|
|
// carrier the carrier needs to have moves left.
|
|
if (unit.getMovesLeft() > 0 && (!unit.isOnCarrier()
|
|
|| unit.getCarrier().getMovesLeft() > 0)) {
|
|
lb.add("+");
|
|
result.add(aiu);
|
|
} else {
|
|
lb.add(".");
|
|
}
|
|
|
|
// Immediately update a newly built colony so that other
|
|
// units that are about to wake up can see its tile
|
|
// improvement plans.
|
|
Colony newColony = unit.getLocation().getColony();
|
|
if (oldColony == null && newColony != null
|
|
&& Map.isSameLocation(oldLocation, newColony)) {
|
|
AIColony aiColony = getAIColony(newColony);
|
|
aiColony.update(lb);
|
|
updateTipMap(aiColony);
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* {@inheritDoc}
|
|
*/
|
|
@Override
|
|
public int adjustMission(AIUnit aiUnit, PathNode path, Class type,
|
|
int value) {
|
|
if (value > 0) {
|
|
if (type == DefendSettlementMission.class) {
|
|
// Reduce value in proportion to the number of defenders.
|
|
Location loc = DefendSettlementMission.extractTarget(aiUnit, path);
|
|
if (!(loc instanceof Colony)) {
|
|
throw new IllegalStateException("European players defend colonies: " + loc);
|
|
}
|
|
Colony colony = (Colony)loc;
|
|
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;
|
|
}
|
|
|
|
/**
|
|
* {@inheritDoc}
|
|
*/
|
|
@Override
|
|
public IndianDemandAction indianDemand(Unit unit, Colony colony,
|
|
GoodsType goods, int gold,
|
|
IndianDemandAction accept) {
|
|
// FIXME: make a better choice, check whether the colony is
|
|
// well defended
|
|
return ("conquest".equals(getAIAdvantage()))
|
|
? IndianDemandAction.INDIAN_DEMAND_ACCEPT
|
|
: IndianDemandAction.INDIAN_DEMAND_REJECT;
|
|
}
|
|
|
|
/**
|
|
* {@inheritDoc}
|
|
*/
|
|
@Override
|
|
public TradeStatus acceptDiplomaticTrade(DiplomaticTrade agreement) {
|
|
final Player player = getPlayer();
|
|
final Player other = agreement.getOtherPlayer(player);
|
|
final boolean franklin
|
|
= other.hasAbility(Ability.ALWAYS_OFFERED_PEACE);
|
|
final java.util.Map<TradeItem, Integer> scores = new HashMap<>();
|
|
TradeItem peace = null;
|
|
TradeStatus result = null;
|
|
LogBuilder lb = new LogBuilder(64);
|
|
lb.add("Evaluate trade offer to ", player.getName(),
|
|
" from ", other.getName());
|
|
if (agreement.getVersion() == 0) {
|
|
// Synthetic event
|
|
result = TradeStatus.PROPOSE_TRADE;
|
|
} else {
|
|
int unacceptable = 0, value = 0, colonies = 0;
|
|
for (TradeItem item : agreement.getItems()) {
|
|
if (item instanceof StanceTradeItem) {
|
|
getNationSummary(other); // Freshen the name summary cache
|
|
}
|
|
int score = item.evaluateFor(player);
|
|
if (item instanceof ColonyTradeItem) {
|
|
if (item.getSource() == player) {
|
|
colonies++;
|
|
} else {
|
|
colonies--;
|
|
}
|
|
} else if (item instanceof StanceTradeItem) {
|
|
// Handle some special cases
|
|
switch (item.getStance()) {
|
|
case ALLIANCE: case CEASE_FIRE:
|
|
if (franklin) {
|
|
peace = item;
|
|
score = 0;
|
|
}
|
|
break;
|
|
case UNCONTACTED: case PEACE:
|
|
if (agreement.getContext() == TradeContext.CONTACT) {
|
|
peace = item;
|
|
score = 0;
|
|
}
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
if (score == TradeItem.INVALID_TRADE_ITEM) {
|
|
unacceptable++;
|
|
} else {
|
|
value += score;
|
|
}
|
|
scores.put(item, score);
|
|
lb.add(", ", Messages.message(item.getLabel()), " = ", score);
|
|
}
|
|
lb.add(".");
|
|
|
|
if (colonies > 0
|
|
&& colonies > player.getSettlementCount() - Colony.TRADE_MARGIN) {
|
|
result = rejectAgreement(peace, agreement);
|
|
lb.add(" Too many (", colonies, ") colonies lost.");
|
|
} else if (unacceptable == 0 && value >= 0) { // Accept if all good
|
|
result = TradeStatus.ACCEPT_TRADE;
|
|
lb.add(" All accepted at ", value, ".");
|
|
} else { // If too many items are unacceptable, reject
|
|
double ratio = (double)unacceptable
|
|
/ (unacceptable + agreement.getItems().size());
|
|
if (ratio > 0.5 - 0.5 * agreement.getVersion()) {
|
|
result = rejectAgreement(peace, agreement);
|
|
lb.add(" Too many (", unacceptable, ") unacceptable.");
|
|
}
|
|
}
|
|
|
|
if (result == null) {
|
|
// Dump the unacceptable offers, sum the rest
|
|
value = 0;
|
|
for (Entry<TradeItem, Integer> entry : scores.entrySet()) {
|
|
if (entry.getValue() == TradeItem.INVALID_TRADE_ITEM) {
|
|
agreement.remove(entry.getKey());
|
|
lb.add(" Dropped invalid ", entry.getKey(), ".");
|
|
} else {
|
|
value += entry.getValue();
|
|
lb.add(" Added valid ", entry.getKey(),
|
|
", total = ", value, ".");
|
|
}
|
|
}
|
|
// If nothing is left then fail,
|
|
if (agreement.isEmpty()) {
|
|
result = rejectAgreement(peace, agreement);
|
|
}
|
|
}
|
|
|
|
// Give up?
|
|
if (randomInt(logger, "Enough diplomacy?", getAIRandom(),
|
|
1 + agreement.getVersion()) > 5) {
|
|
result = rejectAgreement(peace, agreement);
|
|
lb.add(" Ran out of patience at ", agreement.getVersion(), ".");
|
|
}
|
|
|
|
if (result == null) {
|
|
// Dump the negative offers until the sum is non-negative.
|
|
// Return a proposal with items we like/can accept, or reject
|
|
// if none are left.
|
|
for (Entry<TradeItem, Integer> e
|
|
: mapEntriesByValue(scores, ascendingIntegerComparator)) {
|
|
if (value >= 0) break;
|
|
TradeItem item = e.getKey();
|
|
value -= e.getValue();
|
|
if (value >= 50 && item instanceof GoldTradeItem) {
|
|
// Counter offer smaller amount of gold, FIXME: magic#
|
|
GoldTradeItem gti = (GoldTradeItem)item;
|
|
gti.setGold(gti.getGold() - value / 2);
|
|
value /= 2;
|
|
lb.add(" Reducing gold item to ", gti.getGold(), ".");
|
|
} else {
|
|
agreement.remove(item);
|
|
lb.add(" Dropped ", item, ", value now = ", value, ".");
|
|
}
|
|
}
|
|
if (value >= 0 && !agreement.isEmpty()) {
|
|
result = TradeStatus.PROPOSE_TRADE;
|
|
lb.add(" Pruned until acceptable at ", value, ".");
|
|
} else {
|
|
result = rejectAgreement(peace, agreement);
|
|
lb.add(" Agreement unsalvageable at ", value, ".");
|
|
}
|
|
}
|
|
}
|
|
|
|
lb.add(" => ", result);
|
|
lb.log(logger, Level.INFO);
|
|
return result;
|
|
}
|
|
|
|
|
|
/**
|
|
* {@inheritDoc}
|
|
*/
|
|
@Override
|
|
public NativeTradeAction handleTrade(NativeTradeAction action,
|
|
NativeTrade nt) {
|
|
return NativeTradeAction.NAK_INVALID;
|
|
}
|
|
|
|
|
|
/**
|
|
* {@inheritDoc}
|
|
*/
|
|
@Override
|
|
public boolean acceptTax(int tax) {
|
|
boolean ret = true;
|
|
LogBuilder lb = new LogBuilder(64);
|
|
Goods toBeDestroyed = getPlayer().getMostValuableGoods();
|
|
lb.add("Tax demand to ", getPlayer().getName(), " of ", tax, "% with ",
|
|
getPlayer().getMostValuableGoods(), " ");
|
|
GoodsType goodsType = (toBeDestroyed == null) ? null
|
|
: toBeDestroyed.getType();
|
|
|
|
if (tax <= 2) {
|
|
// Accept small increase.
|
|
ret = true;
|
|
lb.add("accepted: small rise.");
|
|
} else if (toBeDestroyed == null) {
|
|
// Is this cheating to look at what the crown will destroy?
|
|
ret = false;
|
|
lb.add("rejected: no-goods-under-threat.");
|
|
} else if (goodsType.isFoodType()) {
|
|
ret = false;
|
|
lb.add("rejected: food-type.");
|
|
} else if (goodsType.isBreedable()) {
|
|
// Refuse if we already have this type under production in
|
|
// multiple places.
|
|
int n = count(getPlayer().getSettlements(),
|
|
s -> s.getGoodsCount(goodsType) > 0);
|
|
ret = n < 2;
|
|
if (ret) {
|
|
lb.add("accepted: breedable-type-", goodsType.getSuffix(),
|
|
"-missing.");
|
|
} else {
|
|
lb.add("rejected: breedable-type-", goodsType.getSuffix(),
|
|
"-present-in-", n, "-settlements.");
|
|
}
|
|
} else if (goodsType.getMilitary()
|
|
|| goodsType.isTradeGoods()
|
|
|| goodsType.isBuildingMaterial()) {
|
|
// By age 3 we should be able to produce enough ourselves.
|
|
// FIXME: check whether we have an armory, at least
|
|
int turn = getGame().getTurn().getNumber();
|
|
ret = turn < 300;
|
|
lb.add(((ret) ? "accepted" : "rejected"),
|
|
": special-goods-in-turn-", turn, ".");
|
|
} else {
|
|
// FIXME: 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.
|
|
final List<GoodsType> goodsTypes = getSpecification()
|
|
.getStorableGoodsTypeList();
|
|
int averageIncome = sum(goodsTypes,
|
|
gt -> getPlayer().getIncomeAfterTaxes(gt)) / goodsTypes.size();
|
|
int income = getPlayer().getIncomeAfterTaxes(toBeDestroyed.getType());
|
|
ret = income <= 0 || income > averageIncome;
|
|
lb.add(((ret) ? "accepted" : "rejected"),
|
|
": goods(", goodsType.getSuffix(), ")-with-income(", income,
|
|
((ret) ? ")non-positive-or-more-than(" : ")less-than-average("),
|
|
averageIncome, ").");
|
|
}
|
|
if (!ret) suppressEuropeanTrade(goodsType, lb);
|
|
lb.log(logger, Level.INFO);
|
|
return ret;
|
|
}
|
|
|
|
/**
|
|
* {@inheritDoc}
|
|
*/
|
|
@Override
|
|
public boolean acceptMercenaries() {
|
|
return getPlayer().isAtWar() || "conquest".equals(getAIAdvantage());
|
|
}
|
|
|
|
/**
|
|
* {@inheritDoc}
|
|
*/
|
|
@Override
|
|
public FoundingFather selectFoundingFather(List<FoundingFather> ffs) {
|
|
final int age = getGame().getAge();
|
|
FoundingFather bestFather = null;
|
|
int bestWeight = Integer.MIN_VALUE;
|
|
for (FoundingFather father : ffs) {
|
|
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(Ability.BUILD_CUSTOM_HOUSE)) {
|
|
bestFather = father;
|
|
break;
|
|
}
|
|
|
|
int weight = father.getWeight(age);
|
|
if (weight > bestWeight) {
|
|
bestWeight = weight;
|
|
bestFather = father;
|
|
}
|
|
}
|
|
return bestFather;
|
|
}
|
|
|
|
/**
|
|
* Gets the needed wagons for a tile/contiguity.
|
|
*
|
|
* @param tile The {@code Tile} to derive the contiguity from.
|
|
* @return The number of wagons needed.
|
|
*/
|
|
public int getNeededWagons(Tile tile) {
|
|
if (tile != null) {
|
|
int contig = tile.getContiguity();
|
|
if (contig > 0) {
|
|
Integer i = wagonsNeeded.get(contig);
|
|
if (i != null) return i;
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* How many pioneers should we have?
|
|
*
|
|
* This is the desired total number, not the actual number which would
|
|
* take into account the number of existing PioneeringMissions.
|
|
*
|
|
* @return The desired number of pioneers for this player.
|
|
*/
|
|
public int pioneersNeeded() {
|
|
return (tipMap.size() + 1) / 2;
|
|
}
|
|
|
|
/**
|
|
* How many scouts should we have?
|
|
*
|
|
* This is the desired total number, not the actual number which would
|
|
* take into account the number of existing ScoutingMissions.
|
|
*
|
|
* 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 3 - (getGame().getTurn().getNumber() / 100);
|
|
}
|
|
|
|
/**
|
|
* Notify that a wish has been completed. Called from AIColony.
|
|
*
|
|
* @param w The {@code Wish} to complete.
|
|
*/
|
|
public void completeWish(Wish w) {
|
|
if (w instanceof WorkerWish) {
|
|
WorkerWish ww = (WorkerWish)w;
|
|
List<WorkerWish> wl = workerWishes.get(ww.getUnitType());
|
|
if (wl != null) wl.remove(ww);
|
|
} else if (w instanceof GoodsWish) {
|
|
GoodsWish gw = (GoodsWish)w;
|
|
List<GoodsWish> gl = goodsWishes.get(gw.getGoodsType());
|
|
if (gl != null) gl.remove(gw);
|
|
} else {
|
|
throw new IllegalStateException("Bogus wish: " + w);
|
|
}
|
|
}
|
|
|
|
|
|
// Serialization
|
|
|
|
// getXMLTagName not needed, uses parent
|
|
}
|