freecol/src/net/sf/freecol/server/ai/NativeAIPlayer.java

988 lines
37 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 java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Random;
import java.util.Set;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.ToIntFunction;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.xml.stream.XMLStreamException;
import net.sf.freecol.common.io.FreeColXMLReader;
import net.sf.freecol.common.model.Colony;
import net.sf.freecol.common.model.CombatModel;
import net.sf.freecol.common.model.Constants.IndianDemandAction;
import net.sf.freecol.common.model.FeatureContainer;
import net.sf.freecol.common.model.Goods;
import net.sf.freecol.common.model.GoodsType;
import net.sf.freecol.common.model.IndianSettlement;
import net.sf.freecol.common.model.Location;
import net.sf.freecol.common.model.Modifier;
import net.sf.freecol.common.model.NativeTrade;
import net.sf.freecol.common.model.NativeTrade.NativeTradeAction;
import net.sf.freecol.common.model.NativeTradeItem;
import net.sf.freecol.common.model.Ownable;
import net.sf.freecol.common.model.PathNode;
import net.sf.freecol.common.model.Player;
import net.sf.freecol.common.model.Stance;
import net.sf.freecol.common.model.Settlement;
import net.sf.freecol.common.model.Specification;
import net.sf.freecol.common.model.Tension;
import net.sf.freecol.common.model.Tile;
import net.sf.freecol.common.model.Turn;
import net.sf.freecol.common.model.Unit;
import net.sf.freecol.common.model.Role;
import net.sf.freecol.common.model.pathfinding.CostDecider;
import net.sf.freecol.common.model.pathfinding.CostDeciders;
import net.sf.freecol.common.option.GameOptions;
import static net.sf.freecol.common.util.CollectionUtils.*;
import net.sf.freecol.common.util.LogBuilder;
import net.sf.freecol.common.util.RandomChoice;
import static net.sf.freecol.common.util.RandomUtils.*;
import net.sf.freecol.server.ai.mission.DefendSettlementMission;
import net.sf.freecol.server.ai.mission.IndianBringGiftMission;
import net.sf.freecol.server.ai.mission.IndianDemandMission;
import net.sf.freecol.server.ai.mission.Mission;
import net.sf.freecol.server.ai.mission.UnitSeekAndDestroyMission;
import net.sf.freecol.server.ai.mission.UnitWanderHostileMission;
/**
* Objects of this class contains AI-information for a single {@link
* Player} and is used for controlling this player.
*
* The method {@link #startWorking} gets called by the
* {@link AIInGameInputHandler} when it is this player's turn.
*/
public final class NativeAIPlayer extends MissionAIPlayer {
private static final Logger logger = Logger.getLogger(NativeAIPlayer.class.getName());
public static final int MAX_DISTANCE_TO_BRING_GIFTS = 5;
public static final int MAX_NUMBER_OF_GIFTS_BEING_DELIVERED = 1;
public static final int MAX_DISTANCE_TO_MAKE_DEMANDS = 5;
public static final int MAX_NUMBER_OF_DEMANDS = 1;
/**
* Debug helper to keep track of why/what the units are doing.
* Do not serialize.
*/
private final java.util.Map<Unit, String> reasons = new HashMap<>();
/**
* Creates a new {@code AIPlayer}.
*
* @param aiMain The main AI-class.
* @param player The player that should be associated with this
* {@code AIPlayer}.
*/
public NativeAIPlayer(AIMain aiMain, Player player) {
super(aiMain, player);
this.initialized = getPlayer() != null;
}
/**
* 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 NativeAIPlayer(AIMain aiMain,
FreeColXMLReader xr) throws XMLStreamException {
super(aiMain, xr);
this.initialized = getPlayer() != null;
}
/**
* 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 Player player = getPlayer();
lb.add("\n Initialize");
// Give defensive missions up to the minimum expected defence,
// leave the rest with the default wander-hostile mission.
for (IndianSettlement is : player.getIndianSettlementList()) {
List<Unit> units = is.getAllUnitsList();
while (units.size() > is.getRequiredDefenders()) {
Unit u = units.remove(0);
AIUnit aiu = getAIUnit(u);
Mission m = getWanderHostileMission(aiu);
if (m != null) lb.add(" ", m);
}
for (Unit u : units) {
AIUnit aiu = getAIUnit(u);
Mission m = getDefendSettlementMission(aiu, is);
if (m != null) lb.add(" ", m);
}
}
}
/**
* Determines the stances towards each player.
* That is: should we declare war?
*
* @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)) {
getAIMain().getFreeColServer().getInGameController()
.changeStance(player, newStance, p, true);
lb.add(" ", p.getDebugName(), "->", newStance, ", ");
}
}
if (lb.grew("\n Stance changes:")) lb.shrink(", ");
}
/**
* Takes the necessary actions to secure the settlements.
* This is done by making new military units or to give existing
* units new missions.
*
* @param randoms An array of random settlement indexes.
* @param lb A {@code LogBuilder} to log to.
*/
private void secureSettlements(int[] randoms, LogBuilder lb) {
int randomIdx = 0;
List<IndianSettlement> settlements
= getPlayer().getIndianSettlementList();
for (IndianSettlement is : settlements) {
// Spread arms and horses between camps
// FIXME: maybe make this dependent on difficulty level?
int n = randoms[randomIdx++];
IndianSettlement settlement = settlements.get(n);
if (settlement != is) {
is.tradeGoodsWithSettlement(settlement);
}
}
for (IndianSettlement is : settlements) {
lb.mark();
equipBraves(is, lb);
secureIndianSettlement(is, lb);
if (lb.grew("\n At ", is.getName())) lb.shrink(", ");
}
}
/**
* Greedily equips braves with horses and muskets.
*
* Public for the test suite.
*
* @param is The {@code IndianSettlement} where the equipping occurs.
* @param lb A {@code LogBuilder} to log to.
*/
public void equipBraves(IndianSettlement is, LogBuilder lb) {
// Prioritize promoting partially equipped units to full dragoon
final Comparator<Unit> comp = getGame().getCombatModel()
.getMilitaryStrengthComparator();
for (Unit u : sort(is.getAllUnitsList(), comp)) {
Role r = is.canImproveUnitMilitaryRole(u);
if (r != null) {
Role old = u.getRole();
if (getAIUnit(u).equipForRole(r) && u.getRole() != old) {
lb.add(u, " upgraded from ", old.getSuffix(), ", ");
}
}
}
}
/**
* Takes the necessary actions to secure an indian settlement
*
* Public for the test suite.
*
* @param is The {@code IndianSettlement} to secure.
* @param lb A {@code LogBuilder} to log to.
*/
public void secureIndianSettlement(final IndianSettlement is,
LogBuilder lb) {
final AIMain aiMain = getAIMain();
final Player player = getPlayer();
final CombatModel cm = getGame().getCombatModel();
final int minimumDefence = is.getType().getMinimumSize() - 1;
DefendSettlementMission dm;
// Collect native units and defenders
List<Unit> units = is.getAllUnitsList();
List<Unit> defenders = new ArrayList<>();
for (Unit u : is.getOwnedUnitList()) {
if (!units.contains(u)) units.add(u);
}
// Collect the current defenders
for (Unit u : new ArrayList<>(units)) {
AIUnit aiu = aiMain.getAIUnit(u);
if (aiu == null) {
units.remove(u);
} else if ((dm = aiu.getMission(DefendSettlementMission.class)) != null
&& dm.getTarget() == is) {
defenders.add(u);
units.remove(u);
} else if (Mission.invalidNewMissionReason(aiu) != null) {
units.remove(u);
}
}
// Collect threats and other potential defenders
final HashMap<Tile, Double> threats = new HashMap<>();
Player enemy;
Tension tension;
for (Tile t : is.getTile().getSurroundingTiles(is.getRadius() + 1)) {
if (!t.isLand() || t.getUnitCount() == 0) {
; // Do nothing
} else if ((enemy = t.getFirstUnit().getOwner()) == player) {
// Its one of ours!
for (Unit u : t.getUnitList()) {
AIUnit aiu;
if (defenders.contains(u) || units.contains(u)
|| (aiu = aiMain.getAIUnit(u)) == null) {
; // Do nothing
} else if ((dm = aiu.getMission(DefendSettlementMission.class)) != null
&& dm.getTarget() == is) {
defenders.add(u);
} else if (Mission.invalidNewMissionReason(aiu) == null) {
units.add(u);
}
}
} else if ((tension = is.getAlarm(enemy)) == null
|| tension.getLevel().compareTo(Tension.Level.CONTENT) <= 0) {
; // Not regarded as a threat
} else {
// Evaluate the threat
double threshold, bonus, value = 0.0;
if (tension.getLevel().compareTo(Tension.Level.DISPLEASED) <= 0) {
threshold = 1.0;
bonus = 0.0f;
} else {
threshold = 0.0;
bonus = (float)tension.getLevel().ordinal()
- Tension.Level.CONTENT.ordinal();
}
value += sumDouble(t.getUnits(),
u -> cm.getOffencePower(u, is) > threshold,
u -> cm.getOffencePower(u, is) + bonus);
if (value > 0.0) threats.put(t, value);
}
}
// Sort the available units by proximity to the settlement.
// Simulates favouring the first warriors found by outgoing messengers.
// Also favour units native to the settlement.
final int homeBonus = 3;
final Tile isTile = is.getTile();
final Comparator<Unit> isComparator = cachingIntComparator(u -> {
final Tile t = u.getTile();
return t.getDistanceTo(isTile)
- ((u.getHomeIndianSettlement() == is) ? homeBonus : 0);
});
// Do we need more or less defenders?
int needed = minimumDefence + threats.size();
if (defenders.size() < needed) { // More needed, call some in.
units.sort(isComparator);
while (!units.isEmpty()) {
Unit u = units.remove(0);
AIUnit aiu = aiMain.getAIUnit(u);
Mission m = getDefendSettlementMission(aiu, is);
if (m != null) {
lb.add(m, ", ");
defenders.add(u);
if (defenders.size() >= needed) break;
}
}
} else if (defenders.size() > needed) { // Less needed, release them
defenders.sort(isComparator.reversed());
while (defenders.size() > needed) {
units.add(defenders.remove(0));
}
}
// Sort threat tiles by threat value.
final Comparator<Tile> threatComp
= Comparator.<Tile>comparingDouble(t -> threats.get(t));
List<Tile> threatTiles = sort(threats.keySet(), threatComp);
if (!defenders.isEmpty()) {
lb.add(" defend with:");
for (Unit u : defenders) lb.add(" ", u);
lb.add(" minimum=", minimumDefence,
" threats=", threats.size(), ", ");
}
// Assign units to attack the threats, greedily chosing closest unit.
while (!threatTiles.isEmpty() && !units.isEmpty()) {
Tile tile = threatTiles.remove(0);
final ToIntFunction<Unit> score = cacheInt(u ->
u.getTile().getDistanceTo(tile));
final Predicate<Unit> validPred = u ->
UnitSeekAndDestroyMission.invalidMissionReason(aiMain.getAIUnit(u),
tile.getDefendingUnit(u)) == null
&& score.applyAsInt(u) >= 0;
final Comparator<Unit> scoreComp = Comparator.comparingInt(score);
Unit unit = minimize(units, validPred, scoreComp);
if (unit == null) continue; // Declined to attack.
units.remove(unit);
AIUnit aiUnit = aiMain.getAIUnit(unit);
Unit target = tile.getDefendingUnit(unit);
Mission m = getSeekAndDestroyMission(aiUnit, target);
if (m != null) lb.add(m, ", ");
}
}
/**
* Gives a mission to all units.
*
* @param lb A {@code LogBuilder} to log to.
*/
private void giveNormalMissions(LogBuilder lb) {
final Player player = getPlayer();
List<AIUnit> aiUnits = getAIUnits();
lb.mark();
List<AIUnit> done = new ArrayList<>(aiUnits.size());
reasons.clear();
for (AIUnit aiUnit : aiUnits) {
final Unit unit = aiUnit.getUnit();
Mission m = aiUnit.getMission();
if (!unit.isInitialized() || unit.isDisposed()) {
reasons.put(unit, "Invalid");
} else if (m != null && m.isValid() && !m.isOneTime()) {
reasons.put(unit, "Valid");
} else { // Unit needs a mission
continue;
}
done.add(aiUnit);
}
aiUnits.removeAll(done);
done.clear();
for (AIUnit aiUnit : aiUnits) {
final Unit unit = aiUnit.getUnit();
final Settlement settlement = unit.getSettlement();
final IndianSettlement is = unit.getHomeIndianSettlement();
Mission m = aiUnit.getMission();
if (settlement != null && settlement.getUnitCount()
+ settlement.getTile().getUnitCount() <= 1) {
// First see to local settlement defence
if (!(m instanceof DefendSettlementMission)
|| m.getTarget() != settlement) {
m = getDefendSettlementMission(aiUnit, settlement);
if (m == null) continue;
lb.add(m, ", ");
}
reasons.put(unit, "Defend-" + settlement.getName());
} else if (is != null
&& is.canImproveUnitMilitaryRole(unit) != null) {
// Go home for new equipment if the home settlement has it
if (!(m instanceof DefendSettlementMission)
|| m.getTarget() != is) {
m = getDefendSettlementMission(aiUnit, is);
if (m == null) continue;
lb.add(m, ", ");
}
reasons.put(unit, "Equip-" + is.getName());
} else {
// Go out looking for trouble
if (!(m instanceof UnitWanderHostileMission)) {
m = getWanderHostileMission(aiUnit);
if (m == null) continue;
lb.add(m, ", ");
}
reasons.put(unit, "Patrol");
}
done.add(aiUnit);
}
aiUnits.removeAll(done);
done.clear();
// Log
if (lb.grew("\n Mission changes: ")) lb.shrink(", ");
if (!aiUnits.isEmpty()) {
lb.add("\n Free Land Units:");
for (AIUnit aiu : aiUnits) lb.add(" ", aiu.getUnit());
}
lb.add("\n Missions(settlements=", player.getSettlementCount(), ")");
logMissions(reasons, lb);
}
/**
* Brings gifts to nice players with nearby colonies.
*
* @param randoms An array of random percentages.
* @param lb A {@code LogBuilder} to log to.
*/
private void bringGifts(int[] randoms, LogBuilder lb) {
final Player player = getPlayer();
final CostDecider cd = CostDeciders.numberOfLegalTiles();
final int giftPercent = getSpecification()
.getPercentage(GameOptions.GIFT_PROBABILITY);
int randomIdx = 0;
lb.mark();
for (IndianSettlement is : player.getIndianSettlementList()) {
// Do not bring gifts all the time.
if (randoms[randomIdx++] >= giftPercent) continue;
// Check if the settlement has anything to give.
Goods gift = is.getRandomGift(getAIRandom());
if (gift == null) continue;
// Check if there are available units, and if there are already
// enough missions in operation.
List<Unit> availableUnits = new ArrayList<>();
int alreadyAssignedUnits = 0;
for (Unit ou : is.getOwnedUnitList()) {
AIUnit aiu = getAIUnit(ou);
if (aiu == null) {
continue;
} else if (aiu.hasMission(IndianBringGiftMission.class)) {
alreadyAssignedUnits++;
} else if (Mission.invalidNewMissionReason(aiu) == null) {
availableUnits.add(ou);
}
}
if (alreadyAssignedUnits > MAX_NUMBER_OF_GIFTS_BEING_DELIVERED) {
lb.add(is.getName(), " has ", alreadyAssignedUnits,
" already, ");
continue;
} else if (availableUnits.isEmpty()) {
lb.add(is.getName(), " has no gift units, ");
continue;
}
// Pick a random available capable unit.
Unit unit = null;
AIUnit aiUnit = null;
Tile home = is.getTile();
while (unit == null && !availableUnits.isEmpty()) {
Unit u = availableUnits.get(randomInt(logger, "Gift unit",
getAIRandom(), availableUnits.size()));
availableUnits.remove(u);
aiUnit = getAIUnit(u);
if (IndianBringGiftMission.invalidMissionReason(aiUnit) == null
&& u.findPath(u.getTile(), home, null, cd, null) != null) {
unit = u;
}
}
if (unit == null) {
lb.add(is.getName(), " found no gift unit, ");
continue;
}
// Collect nearby colonies. Filter out ones which are uncontacted,
// unreachable or otherwise unsuitable. Score the rest on alarm
// and distance.
List<RandomChoice<Colony>> nearbyColonies = new ArrayList<>();
for (Tile t : home.getSurroundingTiles(MAX_DISTANCE_TO_BRING_GIFTS)) {
Colony c = t.getColony();
PathNode path;
if (c == null
|| !is.hasContacted(c.getOwner())
|| IndianBringGiftMission.invalidMissionReason(aiUnit, c) != null
|| (path = unit.findPath(home, c.getTile(),
null, cd, null)) == null) continue;
int alarm = Math.max(1, is.getAlarm(c.getOwner()).getValue());
nearbyColonies.add(new RandomChoice<>(c,
1000000 / alarm / (1 + path.getTotalTurns())));
}
// If there are any suitable colonies, pick a random one
// to send a gift to.
if (nearbyColonies.isEmpty()) {
lb.add(is.getName(), " found no gift colonies, ");
continue;
}
Colony target = RandomChoice.getWeightedRandom(logger,
"Choose gift colony", nearbyColonies, getAIRandom());
if (target == null) {
throw new RuntimeException("No gift target: " + this);
}
// Send the unit.
Mission m = new IndianBringGiftMission(getAIMain(), aiUnit, target);
lb.add(m, " gift from ", is.getName(),
" to ", target.getName(), ", ");
}
if (lb.grew("\n Gifts: ")) lb.shrink(", ");
}
/**
* Demands tribute from nasty players with nearby colonies.
*
* @param randoms An array of random percentages.
* @param lb A {@code LogBuilder} to log to.
*/
private void demandTribute(int[] randoms, LogBuilder lb) {
final Player player = getPlayer();
final CostDecider cd = CostDeciders.numberOfLegalTiles();
final int demandPercent = getSpecification()
.getPercentage(GameOptions.DEMAND_PROBABILITY);
int randomIdx = 0;
lb.mark();
for (IndianSettlement is : player.getIndianSettlementList()) {
// Do not demand tribute all of the time.
if (randoms[randomIdx++] >= demandPercent) continue;
// Check if there are available units, and if there are already
// enough missions in operation.
List<Unit> availableUnits = new ArrayList<>();
int alreadyAssignedUnits = 0;
for (Unit ou : is.getOwnedUnitList()) {
AIUnit aiu = getAIUnit(ou);
if (Mission.invalidNewMissionReason(aiu) == null) {
if (aiu.hasMission(IndianDemandMission.class)) {
alreadyAssignedUnits++;
} else {
availableUnits.add(ou);
}
}
}
if (alreadyAssignedUnits > MAX_NUMBER_OF_DEMANDS) {
lb.add(is.getName(), " has ", alreadyAssignedUnits,
" already, ");
continue;
} else if (availableUnits.isEmpty()) {
lb.add(is.getName(), " has no demand units, ");
continue;
}
// Pick a random available capable unit.
Tile home = is.getTile();
Unit unit = null;
AIUnit aiUnit = null;
while (unit == null && !availableUnits.isEmpty()) {
Unit u = availableUnits.get(randomInt(logger, "Demand unit",
getAIRandom(), availableUnits.size()));
availableUnits.remove(u);
aiUnit = getAIUnit(u);
if (IndianDemandMission.invalidMissionReason(aiUnit) == null
&& u.findPath(u.getTile(), home, null, cd, null) != null) {
unit = u;
}
}
if (unit == null) {
lb.add(is.getName(), " found no demand unit, ");
continue;
}
// Collect nearby colonies. Filter out ones which are unreachable
// or with which the settlement is on adequate terms.
List<RandomChoice<Colony>> nearbyColonies = new ArrayList<>();
for (Tile t : home.getSurroundingTiles(MAX_DISTANCE_TO_MAKE_DEMANDS)) {
Colony c = t.getColony();
PathNode path;
if (c == null
|| !is.hasContacted(c.getOwner())
|| IndianDemandMission.invalidMissionReason(aiUnit, c) != null
|| (path = unit.findPath(home, c.getTile(),
null, cd, null)) == null) continue;
int alarm = is.getAlarm(c.getOwner()).getValue();
int defence = c.getUnitCount() + ((c.getStockade() == null) ? 1
: (c.getStockade().getLevel() * 10));
int weight = 1 + alarm * (1000000 / defence
/ (1 + path.getTotalTurns()));
nearbyColonies.add(new RandomChoice<>(c, weight));
}
// If there are any suitable colonies, pick one to demand from.
// Sometimes a random one, sometimes the weakest, sometimes the
// most annoying.
if (nearbyColonies.isEmpty()) {
lb.add(is.getName(), " found no demand colonies, ");
continue;
}
Colony target = RandomChoice.getWeightedRandom(logger,
"Choose demand colony", nearbyColonies, getAIRandom());
if (target == null) {
lb.add(is.getName(), " found no demand target, ");
continue;
}
// Send the unit.
Mission m = new IndianDemandMission(getAIMain(), aiUnit, target);
lb.add("At ", is.getName(), " ", m,
" will demand of ", target, ", ");
}
if (lb.grew("\n Tribute: ")) lb.shrink(", ");
}
/**
* Gets the appropriate ship trade penalties.
*
* @param sense The sense to apply the modifiers.
* @return The ship trade penalties.
*/
private List<Modifier> getShipTradePenalties(boolean sense) {
final Specification spec = getSpecification();
final int penalty = ((sense) ? 1 : -1)
* spec.getInteger(GameOptions.SHIP_TRADE_PENALTY);
final Function<Modifier, Modifier> mapper
= m -> Modifier.makeModifier(m).setValue(penalty);
return transform(spec.getModifiers(Modifier.SHIP_TRADE_PENALTY),
alwaysTrue(), mapper);
}
/**
* Aborts all the missions which are no longer valid.
*
* Public for the test suite.
*/
public void abortInvalidMissions() {
for (AIUnit au : getAIUnits()) {
Mission mission = au.getMission();
String reason = (mission == null) ? null : mission.invalidReason();
if (reason != null) au.changeMission(null);
}
}
// AIPlayer interface
// Inherit:
// acceptDiplomaticTrade
// acceptTax
// acceptMercenaries
// selectFoundingFather
/**
* {@inheritDoc}
*/
public IndianDemandAction indianDemand(Unit unit, Colony colony,
GoodsType type, int amount,
IndianDemandAction accept) {
final Player player = getPlayer();
AIUnit aiu;
IndianDemandMission mission;
if (unit.getOwner() == player) { // Its one of ours
if ((aiu = getAIUnit(unit)) != null // and its valid and demanding
&& (mission = aiu.getMission(IndianDemandMission.class)) != null
&& accept != IndianDemandAction.INDIAN_DEMAND_DONE) {
mission.setSucceeded(accept == IndianDemandAction.INDIAN_DEMAND_ACCEPT);
}
}
// Once we get here, the demand is settled
return IndianDemandAction.INDIAN_DEMAND_DONE;
}
/**
* {@inheritDoc}
*/
@Override
public void startWorking() {
final Player player = getPlayer();
final Turn turn = getGame().getTurn();
final int nSettlements = player.getSettlementCount();
final Random air = getAIRandom();
LogBuilder lb = new LogBuilder(1024);
lb.add(player.getDebugName(), " in ", turn, "/", turn.getNumber());
clearAIUnits();
determineStances(lb);
List<AIUnit> more;
if (turn.isFirstTurn()) {
initializeMissions(lb);
more = getAIUnits();
} else {
int[] randoms;
abortInvalidMissions();
if (nSettlements > 0) {
randoms = randomInts(logger, "Trades", air,
nSettlements, nSettlements);
secureSettlements(randoms, lb);
randoms = randomInts(logger, "Gifts", air, 100, nSettlements);
bringGifts(randoms, lb);
randoms = randomInts(logger, "Tribute", air, 100, nSettlements);
demandTribute(randoms, lb);
}
giveNormalMissions(lb);
more = doMissions(getAIUnits(), lb);
}
if (!more.isEmpty()) {
abortInvalidMissions();
giveNormalMissions(lb);
doMissions(more, lb);
}
clearAIUnits();
lb.log(logger, Level.FINEST);
}
/**
* {@inheritDoc}
*/
@Override
public int adjustMission(AIUnit aiUnit, PathNode path, Class type,
int value) {
if (type == DefendSettlementMission.class) {
// Reduce value in proportion to the number of active defenders.
Settlement settlement = (Settlement)DefendSettlementMission
.extractTarget(aiUnit, path);
value -= 75 * getSettlementDefenders(settlement);
} else if (type == UnitSeekAndDestroyMission.class) {
// Natives prefer to attack when DISPLEASED.
Location target = UnitSeekAndDestroyMission
.extractTarget(aiUnit, path);
Player targetPlayer = (target instanceof Ownable)
? ((Ownable)target).getOwner()
: null;
IndianSettlement is = aiUnit.getUnit().getHomeIndianSettlement();
if (targetPlayer != null
&& is != null && is.getAlarm(targetPlayer) != null) {
value += is.getAlarm(targetPlayer).getValue()
- Tension.Level.DISPLEASED.getLimit();
}
}
return value;
}
/**
* Price the goods to buy.
*
* @param nt The {@code NativeTrade} to update.
* @param anger A penalty to the native prices due to anger.
*/
private void updateTrade(NativeTrade nt, int anger) {
final Specification spec = getSpecification();
final Turn turn = getGame().getTurn();
final IndianSettlement is = nt.getIndianSettlement();
final Unit unit = nt.getUnit();
Set<Modifier> modifiers = new HashSet<>();
if (is.hasMissionary(unit.getOwner())
&& spec.getBoolean(GameOptions.ENHANCED_MISSIONARIES)) {
Unit u = is.getMissionary();
modifiers.addAll(u.getMissionaryTradeModifiers(true));
}
if (unit.isNaval()) {
modifiers.addAll(getShipTradePenalties(true));
}
for (NativeTradeItem nti : nt.getUnitToSettlement()) {
int price = (int)FeatureContainer.applyModifiers(1.0f / anger
* is.getPriceToBuy(nti.getGoods()), turn, modifiers);
for (int h = nti.getHaggleCount(); h >= 0; h--) {
price = NativeTrade.haggleUp(price);
}
if (price <= NativeTradeItem.PRICE_UNSET) {
price = NativeTradeItem.PRICE_INVALID;
}
nti.setPrice(price);
}
modifiers.clear();
if (is.hasMissionary(unit.getOwner())
&& spec.getBoolean(GameOptions.ENHANCED_MISSIONARIES)) {
Unit u = is.getMissionary();
modifiers.addAll(u.getMissionaryTradeModifiers(false));
}
if (unit.isNaval()) {
modifiers.addAll(getShipTradePenalties(false));
}
for (NativeTradeItem nti : nt.getSettlementToUnit()) {
int price = (int)FeatureContainer.applyModifiers((float)anger
* is.getPriceToSell(nti.getGoods()), turn, modifiers);
for (int h = nti.getHaggleCount(); h >= 0; h--) {
price = NativeTrade.haggleDown(price);
}
if (price <= NativeTradeItem.PRICE_UNSET) {
price = NativeTradeItem.PRICE_INVALID;
}
nti.setPrice(price);
}
}
/**
* {@inheritDoc}
*/
@Override
public NativeTradeAction handleTrade(NativeTradeAction action,
NativeTrade nt) {
final int HAGGLE_NUMBER = 3;
if (nt == null || !this.getPlayer().owns(nt.getIndianSettlement())) {
return NativeTradeAction.NAK_INVALID;
}
final IndianSettlement is = nt.getIndianSettlement();
final Unit unit = nt.getUnit();
final Player other = unit.getOwner();
NativeTradeItem ours;
int anger, haggle;
Tension tension = is.getAlarm(other);
if (tension == null) return NativeTradeAction.NAK_INVALID;
switch (action) {
case OPEN:
switch (tension.getLevel()) {
case HAPPY: case CONTENT:
anger = 1;
break;
case DISPLEASED:
anger = 2;
break;
case ANGRY: default:
anger = -1;
break;
}
if (anger < 0) return NativeTradeAction.NAK_HOSTILE;
updateTrade(nt, anger);
return (nt.getDone()) ? NativeTradeAction.NAK_NOSALE
: NativeTradeAction.ACK_OPEN;
case BUY:
switch (tension.getLevel()) {
case HAPPY: case CONTENT:
anger = 1;
break;
case DISPLEASED:
anger = 2;
break;
case ANGRY: default:
anger = -1;
break;
}
if (anger < 0) return NativeTradeAction.NAK_HOSTILE;
updateTrade(nt, anger);
ours = find(nt.getSettlementToUnit(), nt.getItem().goodsMatcher());
if (ours == null) return NativeTradeAction.NAK_INVALID;
if (nt.getItem().priceIsSet()
&& nt.getItem().getPrice() >= ours.getPrice()) {
return NativeTradeAction.ACK_BUY;
}
haggle = ours.getHaggleCount() + 1;
if (randomInt(logger, "Haggle-buy", getAIRandom(),
HAGGLE_NUMBER + haggle) >= HAGGLE_NUMBER) {
return NativeTradeAction.NAK_HAGGLE;
}
ours.setHaggleCount(haggle);
updateTrade(nt, anger);
nt.setItem(ours);
return NativeTradeAction.ACK_BUY_HAGGLE;
case SELL:
switch (tension.getLevel()) {
case HAPPY: case CONTENT:
anger = 1;
break;
case DISPLEASED:
anger = 2;
break;
case ANGRY:
anger = (any(nt.getUnitToSettlement(),
nti -> nti.getGoods().getType().getMilitary()))
? 3 : -1;
break;
default:
anger = -1;
break;
}
if (anger < 0) return NativeTradeAction.NAK_HOSTILE;
updateTrade(nt, anger);
ours = find(nt.getUnitToSettlement(), nt.getItem().goodsMatcher());
if (ours == null) return NativeTradeAction.NAK_INVALID;
if (nt.getItem().priceIsSet()
&& nt.getItem().getPrice() <= ours.getPrice()) {
return NativeTradeAction.ACK_SELL;
}
haggle = ours.getHaggleCount() + 1;
if (randomInt(logger, "Haggle-sell", getAIRandom(),
HAGGLE_NUMBER + haggle) >= HAGGLE_NUMBER) {
return NativeTradeAction.NAK_HAGGLE;
}
ours.setHaggleCount(haggle);
updateTrade(nt, anger);
nt.setItem(ours);
return NativeTradeAction.ACK_SELL_HAGGLE;
case GIFT:
ours = find(nt.getUnitToSettlement(), nt.getItem().goodsMatcher());
if (ours == null) return NativeTradeAction.NAK_INVALID;
return (is.canAdd(nt.getItem().getGoods()))
? NativeTradeAction.ACK_GIFT
: NativeTradeAction.NAK_GOODS;
default: // Invalid
return NativeTradeAction.NAK_INVALID;
}
}
/**
* {@inheritDoc}
*/
public int getNeededWagons(Tile tile) {
throw new RuntimeException("Can not happen: " + this);
}
/**
* {@inheritDoc}
*/
public int pioneersNeeded() {
throw new RuntimeException("Can not happen: " + this);
}
/**
* {@inheritDoc}
*/
public int scoutsNeeded() {
throw new RuntimeException("Can not happen: " + this);
}
/**
* {@inheritDoc}
*/
public void completeWish(Wish w) {
throw new RuntimeException("Can not happen: " + this);
}
// Serialization
// getXMLTagName not needed, uses parent
}