/**
* Copyright (C) 2002-2012 The FreeCol Team
*
* This file is part of FreeCol.
*
* FreeCol is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* FreeCol is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with FreeCol. If not, see <http://www.gnu.org/licenses/>.
*/
package net.sf.freecol.server.ai;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Random;
import java.util.logging.Level;
import java.util.logging.Logger;
import net.sf.freecol.common.model.Colony;
import net.sf.freecol.common.model.DiplomaticTrade;
import net.sf.freecol.common.model.FoundingFather;
import net.sf.freecol.common.model.Goods;
import net.sf.freecol.common.model.Player;
import net.sf.freecol.common.model.Player.PlayerType;
import net.sf.freecol.common.model.Player.Stance;
import net.sf.freecol.common.model.Settlement;
import net.sf.freecol.common.model.Tension;
import net.sf.freecol.common.model.Tile;
import net.sf.freecol.common.model.Unit;
import net.sf.freecol.common.networking.Connection;
import net.sf.freecol.server.ai.mission.Mission;
import net.sf.freecol.server.model.ServerPlayer;
import net.sf.freecol.server.networking.DummyConnection;
import org.freecolandroid.xml.stream.XMLStreamException;
import org.freecolandroid.xml.stream.XMLStreamReader;
import org.freecolandroid.xml.stream.XMLStreamWriter;
/**
* 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 abstract class AIPlayer extends AIObject {
private static final Logger logger = Logger.getLogger(AIPlayer.class.getName());
public static final int MAX_DISTANCE_TO_BRING_GIFT = 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;
/**
* The FreeColGameObject this AIObject contains AI-information for.
*/
private ServerPlayer player;
// A PRNG to use for this AI player.
private Random aiRandom = null;
/**
* Temporary variable, used for debugging purposes only.
* See setDebuggingConnection()
*/
private Connection debuggingConnection;
/**
* Temporary variable, used to hold all AIUnit objects belonging to this AI.
* Any implementation of AIPlayer needs to make sure this list is invalidated
* as necessary, using clearAIUnits(), so that getAIUnitIterator() will
* create a new list.
*/
private List<AIUnit> aiUnits = new ArrayList<AIUnit>();
public AIPlayer(AIMain aiMain, String id) {
super(aiMain, id);
aiRandom = new Random(aiMain.getRandomSeed("Seed for " + id));
}
/**
* Returns the <code>Player</code> this <code>AIPlayer</code> is
* controlling.
*
* @return The <code>Player</code>.
*/
public Player getPlayer() {
return player;
}
/**
* Sets the ServerPlayer this AIPlayer is controlling.
* Used by implementing subclasses.
*/
protected void setPlayer(ServerPlayer p) {
player = p;
}
/**
* Returns the ID for this <code>AIPlayer</code>. This is the same as the
* ID for the {@link Player} this <code>AIPlayer</code> controls.
*
* @return The ID.
*/
@Override
public String getId() {
return player.getId();
}
/**
* Gets the PRNG to use for this player.
*
* @return A <code>Random<code> to use for this player.
*/
public Random getAIRandom() {
return aiRandom;
}
/**
* Gets the advantage of this AI player from the nation type.
*
* @return A short string stating the national advantage.
*/
protected String getAIAdvantage() {
final String prefix = "model.nationType.";
String id = (player == null || player.getNationType() == null) ? ""
: player.getNationType().getId();
return (id.startsWith(prefix)) ? id.substring(prefix.length())
: "";
}
/**
* Gets the connection to the server.
*
* @return The connection that can be used when communication with the
* server.
*/
public Connection getConnection() {
if (debuggingConnection != null) {
return debuggingConnection;
} else {
return ((DummyConnection) player.getConnection()).getOtherConnection();
}
}
/**
* Sets the <code>Connection</code> to be used while communicating with
* the server.
*
* This method is only used for debugging.
*
* @param debuggingConnection The connection to be used for debugging.
*/
public void setDebuggingConnection(Connection debuggingConnection) {
this.debuggingConnection = debuggingConnection;
}
/**
* Clears the cache of AI units.
*/
protected void clearAIUnits() {
aiUnits.clear();
}
/**
* Build the cache of AI units.
*/
private void createAIUnits() {
clearAIUnits();
for (Unit u : getPlayer().getUnits()) {
AIUnit a = getAIUnit(u);
if (a != null) {
aiUnits.add(a);
} else {
logger.warning("Could not find the AIUnit for: "
+ u + " (" + u.getId() + ")");
}
}
}
/**
* Gets the AI colony corresponding to a given colony, if any.
*
* @param colony The <code>Colony</code> to look up.
* @return The corresponding AI colony or null if not found.
*/
protected AIColony getAIColony(Colony colony) {
return getAIMain().getAIColony(colony);
}
/**
* Gets a list of the players AI colonies.
*
* @return A list of AI colonies.
*/
protected List<AIColony> getAIColonies() {
List<AIColony> ac = new ArrayList<AIColony>();
for (Colony colony : getPlayer().getColonies()) {
AIColony a = getAIColony(colony);
if (a != null) {
ac.add(a);
} else {
logger.warning("Could not find the AIColony for: " + colony);
}
}
return ac;
}
/**
* Gets the AI unit corresponding to a given unit, if any.
*
* @param unit The <code>Unit</code> to look up.
* @return The corresponding AI unit or null if not found.
*/
protected AIUnit getAIUnit(Unit unit) {
return getAIMain().getAIUnit(unit);
}
/**
* Gets a list of AIUnits for the player.
*
* @return A list of AIUnits.
*/
protected List<AIUnit> getAIUnits() {
if (aiUnits.size() == 0) createAIUnits();
return new ArrayList<AIUnit>(aiUnits);
}
/**
* Returns an iterator over all the <code>AIUnit</code>s owned by this
* player.
*
* @return The <code>Iterator</code>.
*/
protected Iterator<AIUnit> getAIUnitIterator() {
if (aiUnits.size() == 0) createAIUnits();
return aiUnits.iterator();
}
/**
* Standard stance change determination. If a change occurs,
* contact the server and propagate.
*
* @param other The <code>Player</code> wrt consider stance.
* @return The stance, which may have been updated.
*/
protected Stance determineStance(Player other) {
Player player = getPlayer();
Stance newStance;
if (other.getREFPlayer() == player
&& other.getPlayerType() == PlayerType.REBEL) {
newStance = Stance.WAR;
} else {
newStance = player.getStance(other)
.getStanceFromTension(player.getTension(other));
}
if (newStance != player.getStance(other)) {
getAIMain().getFreeColServer().getInGameController()
.changeStance(player, newStance, other, true);
}
return player.getStance(other);
}
/* INTERFACE ******************************************************************/
/**
* Tells this <code>AIPlayer</code> to make decisions. The
* <code>AIPlayer</code> is done doing work this turn when this method
* returns.
*/
public abstract void startWorking();
/**
* Resolves a native demand.
* One of goods/gold is significant.
* Overridden by the European player.
*
* @param unit The native <code>Unit</code> making the demand.
* @param colony The <code>Colony</code> being demanded of.
* @param goods The <code>Goods</code> demanded (may be null).
* @param gold The gold demanded (invalid if goods non-null).
* @return The response of the player.
*/
public boolean indianDemand(Unit unit, Colony colony,
Goods goods, int gold) {
return false;
}
public abstract boolean acceptDiplomaticTrade(DiplomaticTrade agreement);
/**
* Called after another <code>Player</code> sends a
* <code>trade</code> message
*
* @param goods The goods which we are going to offer
*/
public abstract void registerSellGoods(Goods goods);
/**
* Called when another <code>Player</code> proposes to buy.
*
*
* @param unit The foreign <code>Unit</code> trying to trade.
* @param settlement The <code>Settlement</code> this player owns and
* which the given <code>Unit</code> is trading.
* @param goods The goods the given <code>Unit</code> is trying to sell.
* @param gold The suggested price.
* @return The price this <code>AIPlayer</code> suggests or
* {@link net.sf.freecol.common.networking.NetworkConstants#NO_TRADE}.
*/
public abstract int buyProposition(Unit unit, Settlement settlement, Goods goods, int gold);
/**
* Called when another <code>Player</code> proposes a sale.
*
*
* @param unit The foreign <code>Unit</code> trying to trade.
* @param settlement The <code>Settlement</code> this player owns and
* which the given <code>Unit</code> if trying to sell goods.
* @param goods The goods the given <code>Unit</code> is trying to sell.
* @param gold The suggested price.
* @return The price this <code>AIPlayer</code> suggests or
* {@link net.sf.freecol.common.networking.NetworkConstants#NO_TRADE}.
*/
public abstract int sellProposition(Unit unit, Settlement settlement, Goods goods, int gold);
/**
* Decides to accept a tax raise or not.
* Overridden by the European player.
*
* @param tax The tax raise.
* @return True if the raise is accepted.
*/
public boolean acceptTax(int tax) {
return false;
}
/**
* Decides to accept an offer of mercenaries or not.
* Overridden by the European player.
*
* @return True if the mercenaries are accepted.
*/
public boolean acceptMercenaries() {
return false;
}
/**
* Determines the stances towards each player.
* That is: should we declare war?
* TODO: something better, that includes peacemaking.
*/
protected void determineStances() {
logger.finest("Entering method determineStances");
Player player = getPlayer();
for (Player p : getGame().getPlayers()) {
if (p != player && !p.isDead()) determineStance(p);
}
}
/**
* Aborts the mission for a unit, but tries to recover the unit onto
* a neighbouring carrier.
*
* @param aiU The <code>AIUnit</code> whose mission is ended.
* @param why A reason for aborting the mission.
*/
private void abortUnitMission(AIUnit aiU, String why) {
aiU.abortMission(why);
// There is a common stuffup where the AIs converge on the
// same location on a small island. First mover gets the
// colony, the rest get stuck there when their mission is aborted.
//
// TODO: drop this when a more general `marooned unit rescue'
// mission is written.
Unit unit = aiU.getUnit();
Tile tile = unit.getTile();
if (!unit.isCarrier() && !unit.isOnCarrier() && tile != null
&& tile.getColony() == null) {
for (Tile t : tile.getSurroundingTiles(1)) {
for (Unit u : t.getUnitList()) {
if (u.getOwner() == unit.getOwner()
&& u.isCarrier() && u.canCarryUnits()
&& u.getSpaceLeft() >= unit.getSpaceTaken()
&& unit.getMovesLeft() > 0) {
AIMessage.askEmbark(getAIMain().getAIUnit(u), unit,
tile.getDirection(t));
return; // Let the carrier update its transport list.
}
}
}
}
}
/**
* Aborts all the missions which are no longer valid.
*/
protected void abortInvalidMissions() {
for (AIUnit au : getAIUnits()) {
Mission mission = au.getMission();
if (mission != null && !mission.isValid()) {
abortUnitMission(au, "invalid");
}
}
}
/**
* Aborts all the missions which are no longer valid.
*/
protected void abortInvalidAndOneTimeMissions() {
for (AIUnit au : getAIUnits()) {
Mission mission = au.getMission();
if (mission == null) continue;
if (!mission.isValid()) {
abortUnitMission(au, "invalid");
} else if (mission.isOneTime()) {
abortUnitMission(au, "one-time");
}
}
}
/**
* Makes every unit perform their mission.
*/
protected void doMissions() {
logger.finest("Entering method doMissions");
for (AIUnit au : getAIUnits()) {
if (au.hasMission() && au.getMission().isValid()
&& !au.getUnit().isOnCarrier()) {
try {
au.doMission(getConnection());
} catch (Exception e) {
logger.log(Level.WARNING, "doMissions failed", e);
}
}
}
}
/**
* Find out if a tile contains a suitable target for seek-and-destroy.
* TODO: Package for access by a test only - necessary?
*
* @param attacker The attacking <code>Unit</code>.
* @param tile The <code>Tile</code> to attack into.
* @return True if an attack can be launched.
*/
public boolean isTargetValidForSeekAndDestroy(Unit attacker, Tile tile) {
Player attackerPlayer = attacker.getOwner();
// Insist the attacker exists.
if (attacker == null) return false;
// Determine the defending player.
Settlement settlement = tile.getSettlement();
Unit defender = tile.getDefendingUnit(attacker);
Player defenderPlayer = (settlement != null) ? settlement.getOwner()
: (defender != null) ? defender.getOwner()
: null;
// Insist there be a defending player.
if (defenderPlayer == null) return false;
// Can not attack our own units.
if (attackerPlayer == defenderPlayer) return false;
// If European, do not attack if not at war.
// If native, do not attack if not at war and at least content.
// Otherwise some attacks are allowed even if not at war.
boolean atWar = attackerPlayer.atWarWith(defenderPlayer);
if (attackerPlayer.isEuropean()) {
if (!atWar) return false;
} else if (attackerPlayer.isIndian()) {
if (!atWar && attackerPlayer.getTension(defenderPlayer)
.getLevel().compareTo(Tension.Level.CONTENT) <= 0) {
return false;
}
}
// A naval unit can never attack a land unit or settlement,
// but a land unit *can* attack a naval unit if it is on land.
// Otherwise naval units can only fight at sea, land units
// only on land.
if (attacker.isNaval()) {
if (settlement != null
|| !defender.isNaval() || defender.getTile().isLand()) {
return false;
}
} else {
if (defender != null && !defender.getTile().isLand()) {
return false;
}
}
// Otherwise, attack.
return true;
}
/**
* Selects the most useful founding father offered.
* Overridden by EuropeanAIPlayers.
*
* @param ffs The founding fathers on offer.
* @return The founding father selected.
*/
public FoundingFather selectFoundingFather(List<FoundingFather> ffs) {
return null;
}
/**
* Writes this object to an XML stream.
*
* @param out The target stream.
* @throws XMLStreamException if there are any problems writing to the
* stream.
*/
@Override
protected void toXMLImpl(XMLStreamWriter out) throws XMLStreamException {
out.writeStartElement(getXMLElementTagName());
out.writeAttribute(ID_ATTRIBUTE, getId());
out.writeEndElement();
}
/**
* Reads information for this object from an XML stream.
*
* @param in The input stream with the XML.
* @throws XMLStreamException if there are any problems reading from the
* stream.
*/
@Override
protected void readFromXMLImpl(XMLStreamReader in)
throws XMLStreamException {
setPlayer((ServerPlayer) getAIMain()
.getFreeColGameObject(in.getAttributeValue(null, ID_ATTRIBUTE)));
in.nextTag();
}
/**
* Returns the tag name of the root element representing this object.
*
* @return the tag name.
*/
public static String getXMLElementTagName() {
return "aiPlayer";
}
}