/**
* 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.logging.Logger;
import net.sf.freecol.common.model.Colony;
import net.sf.freecol.common.model.Location;
import net.sf.freecol.common.model.PathNode;
import net.sf.freecol.common.model.Player;
import net.sf.freecol.common.model.Settlement;
import net.sf.freecol.common.model.Tile;
import net.sf.freecol.common.model.Unit;
import net.sf.freecol.common.model.pathfinding.GoalDecider;
import net.sf.freecol.server.ai.mission.Mission;
import net.sf.freecol.server.ai.mission.TransportMission;
import net.sf.freecol.server.ai.mission.UnitSeekAndDestroyMission;
import net.sf.freecol.server.model.ServerPlayer;
import org.freecolandroid.xml.stream.XMLStreamException;
import org.freecolandroid.xml.stream.XMLStreamReader;
import org.w3c.dom.Element;
/**
* Objects of this class contains AI-information for a single REF player.
*
* For now, mostly just the EuropeanAIPlayer, with a few tweaks.
*/
public class REFAIPlayer extends EuropeanAIPlayer {
private static final Logger logger = Logger.getLogger(REFAIPlayer.class.getName());
/**
* Creates a new <code>REFAIPlayer</code>.
*
* @param aiMain The main AI-class.
* @param player The player that should be associated with this
* <code>REFAIPlayer</code>.
*/
public REFAIPlayer(AIMain aiMain, ServerPlayer player) {
super(aiMain, player);
}
/**
* Creates a new <code>REFAIPlayer</code> and reads the
* information from the given <code>Element</code>.
*
* @param aiMain The main AI-class.
* @param element The XML-element containing information.
*/
public REFAIPlayer(AIMain aiMain, Element element) {
super(aiMain, element);
}
/**
* Creates a new <code>REFAIPlayer</code>.
*
* @param aiMain The main AI-object.
* @param in The input stream containing the XML.
* @throws XMLStreamException if a problem was encountered during parsing.
*/
public REFAIPlayer(AIMain aiMain, XMLStreamReader in)
throws XMLStreamException {
super(aiMain, in);
}
/**
* Tells this <code>REFAIPlayer</code> to make decisions.
*/
public void startWorking() {
final Player player = getPlayer();
logger.finest("Entering method startWorking: "
+ player + ", year " + getGame().getTurn());
if (!player.isWorkForREF()) {
logger.warning("No work for REF: " + player);
return;
}
super.startWorking();
}
/**
* Initialize the REF.
* - Find the initial target
* - Give valid missions to all the units
*
* Note that we can not rely on normal AI processing as the
* "teleporting" Col1-style REF needs to be placed on the map
* before its first turn starts, so the server should ask this
* AI where it should arrive on the map.
*
* Note also that to find a target we can not just call
* getMilitaryMission and aim for it as the getMilitaryMission
* scoring includes distance from starting point, which is what we
* are trying to determine.
* So, just choose the best coastal colony.
*
* FIXME: Mission assignment is done here because ATM the European
* AI is prone to send ships full of troops off to attack the
* rebel navy. If this is fixed check if the normal mission
* assignment works and drop it from here.
*
* @param teleport "Teleporting" in is allowed.
* @return The preferred entry location, or null if no useful preference.
*/
public Tile initialize(boolean teleport) {
// Find a representative offensive land unit to use to search
// for the initial target.
AIUnit aiUnit = null;
for (AIUnit aiu : getAIUnits()) {
if (!aiu.getUnit().isNaval() && aiu.getUnit().isOffensiveUnit()) {
aiUnit = aiu;
break;
}
}
if (aiUnit == null) {
logger.warning("New REF has no army?!?");
return null;
}
Unit unit = aiUnit.getUnit();
// Find the best coastal colony.
Colony target = null;
int bestScore = Integer.MIN_VALUE;
Player player = getPlayer();
for (Player p : player.getRebels()) {
for (Colony c : p.getColonies()) {
if (c.isConnected()) {
int score = getUnitSeekAndDestroyMissionValue(unit,
c.getTile(), INFINITY);
if (score > bestScore) {
bestScore = score;
target = c;
}
}
}
}
if (target == null) {
logger.warning("Rebels have no connected colonies?!?");
return null;
}
Tile tile = target.getTile();
// Give the army seek-and-destroy missions for the target,
// then once the army has missions it is possible to give the
// navy valid transport missions where needed.
final AIMain aiMain = getAIMain();
for (AIUnit aiu : getAIUnits()) {
if (!aiu.getUnit().isNaval()) {
aiu.setMission(new UnitSeekAndDestroyMission(aiMain, aiu,
target));
}
}
for (AIUnit aiu : getAIUnits()) {
if (aiu.getUnit().isNaval()) {
Unit ship = aiu.getUnit();
if (ship.getUnitCount() > 0) {
TransportMission tm = new TransportMission(aiMain, aiu);
for (Unit u : ship.getUnitList()) {
tm.addToTransportList(aiMain.getAIUnit(u));
}
aiu.setMission(tm);
}
}
}
// Search from the target position to find a Tile to disembark
// to, which must be:
// - Unoccupied
// - Have an unoccupied connected neighbour
//
// TODO: pick the tile with the best defence and try to avoid
// hostile fortifications.
GoalDecider gd = new GoalDecider() {
private PathNode goal = null;
public PathNode getGoal() {
return goal;
}
public boolean hasSubGoals() {
return false;
}
public boolean check(Unit u, PathNode pathNode) {
if (!pathNode.getTile().isEmpty()) return false;
for (Tile t : pathNode.getTile().getSurroundingTiles(1)) {
if (t.isConnected() && t.isEmpty()) {
goal = pathNode;
return true;
}
}
return false;
}
};
PathNode path = unit.search(tile, gd, null, 10, null);
if (path == null) {
logger.warning("Can not find suitable REF landing site for: "
+ target);
return null;
}
tile = path.getTile();
// If teleporting in, the connected tile is an acceptable target.
for (Tile t : tile.getSurroundingTiles(1)) {
if (t.isConnected() && t.isEmpty()) {
tile = t;
break;
}
}
if (teleport) return tile;
// Unit should be aboard a man-o-war which we can use to find a
// path to Europe. Use the end of that path.
if (unit.getLocation() instanceof Unit) {
path = ((Unit)unit).findPathToEurope(tile);
if (path == null) {
logger.warning("Can not find path to Europe from: " + tile);
return null;
}
return path.getLastNode().getTile();
}
logger.warning("REF land unit not aboard a ship: " + unit);
return null;
}
/**
* Evaluate allocating a unit to the defence of a colony.
* Temporary helper method for giveMilitaryMission.
*
* Only public for testAssignDefendSettlementMission.
*
* @param unit The <code>Unit</code> that is to defend.
* @param colony The <code>Colony</code> to defend.
* @param turns The turns for the unit to reach the colony.
* @return A value for such a mission.
*/
@Override
public int getDefendColonyMissionValue(Unit unit, Colony colony,
int turns) {
int value = super.getDefendColonyMissionValue(unit, colony, turns);
// The REF garrisons thinly.
return (getColonyDefenders(colony) > 0) ? 0 : value;
}
/**
* Evaluate a potential seek and destroy mission for a given unit
* to a given tile.
*
* @param unit The <code>Unit</code> to do the mission.
* @param newTile The <code>Tile</code> to go to.
* @param turns How long to travel to the tile.
* @return A score for the proposed mission.
*/
@Override
public int getUnitSeekAndDestroyMissionValue(Unit unit, Tile newTile,
int turns) {
int value = super.getUnitSeekAndDestroyMissionValue(unit, newTile,
turns);
if (value <= 0) return value;
Settlement settlement = newTile.getSettlement();
Unit defender = newTile.getDefendingUnit(unit);
if (settlement == null) {
if (unit.getOwner().getNumberOfSettlements() <= 0) {
// Do not chase units until at least one colony is captured.
return Integer.MIN_VALUE;
}
// Do not all chase the one unit!
if (alreadySeeking(defender)) return Integer.MIN_VALUE;
// The REF is more interested in colonies.
value /= 2;
} else {
if (settlement.isConnected()) value += 1000;
}
return value;
}
/**
* Checks if there is already a seek and destroy mission active on
* for a target unit.
*
* @param unit The <code>Unit</code> to check if there is a mission for.
* @return True if there is a mission for the unit.
*/
private boolean alreadySeeking(Unit unit) {
for (AIUnit au : getAIUnits()) {
Mission m = au.getMission();
Location target;
if (m != null
&& m instanceof UnitSeekAndDestroyMission
&& (target = ((UnitSeekAndDestroyMission) m).getTarget()) != null
&& target instanceof Unit
&& ((Unit)target) == unit) return true;
}
return false;
}
/**
* Gives a mission to non-naval units.
*/
@Override
public void giveNormalMissions() {
// Give military missions to all offensive units.
for (AIUnit aiu : getAIUnits()) {
Unit u = aiu.getUnit();
if (u.isNaval() || aiu.hasMission()) continue;
if (u.isOffensiveUnit()) giveMilitaryMission(aiu);
}
// Fall back to the normal EuropeanAI behaviour for non-army.
super.giveNormalMissions();
}
}