/**
* 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/>.
*/
/**********************************************
* Please see "Howto" at the end of this file! *
**********************************************/
package net.sf.freecol.server.ai.goal;
import java.util.Iterator;
import java.util.List;
import java.util.ArrayList;
import java.util.logging.Logger;
import org.freecolandroid.xml.stream.XMLStreamException;
import org.freecolandroid.xml.stream.XMLStreamReader;
import org.freecolandroid.xml.stream.XMLStreamWriter;
import net.sf.freecol.common.model.GoodsType;
import net.sf.freecol.common.model.UnitType;
import net.sf.freecol.server.ai.AIObject;
import net.sf.freecol.server.ai.AIPlayer;
import net.sf.freecol.server.ai.AIUnit;
import net.sf.freecol.server.ai.goal.GoalConstants;
/**
* A Goal is used to encapsulate a specific part of the
* decision-making process of an AI.
* </p><p>
* Using a top-down approach, every {@link AIPlayer} has a set of Goals which,
* in turn, may have further subgoals. In combination, this tree of goals
* and subgoals reflects the current strategy of the AIPlayer.
* </p><p>
* Units (each one wrapped in an {@link AIUnit} object) will be moved between
* existing Goal objects.
* TODO: Turn {@link AIUnit} into a simple wrapper for individual units.
* </p><p>
* Specific AI goals are created by extending this class; some of
* these could also be used to assist the human player (i.e. GoTo,
* Scouting, Trade, Piracy).
*/
public abstract class Goal extends AIObject implements GoalConstants {
private static final Logger logger = Logger.getLogger(Goal.class.getName());
private float relativeWeight;
protected boolean needsPlanning;
protected boolean isFinished;
protected List<AIUnit> availableUnitsList;
protected AIPlayer player;
private Goal parentGoal;
/**
* Standard constructor
*
* @param p The {@link AIPlayer} this goal belongs to
* @param g The parent goal; may be null if we're a direct goal of the AIPlayer
* @param w The relativeWeight of this goal
*/
public Goal(AIPlayer p, Goal g, float w) {
super(p.getAIMain());
player = p;
parentGoal = g;
relativeWeight = w;
getGame().getTurn().getNumber();
needsPlanning = true; //a newly created Goal always needs planning
isFinished = false; //only plan() should set this to true!
availableUnitsList = new ArrayList<AIUnit>();
}
/**
* Alternate constructor - directly add a unit to this Goal.
* The calling object ensures that this unit is not currently part of another Goal.
*
* @param p The {@link AIPlayer} this goal belongs to
* @param g The parent goal; may be null if we're a direct goal of the AIPlayer
* @param w The relativeWeight of this goal
* @param u An initial {@link AIUnit} given to this goal
*/
public Goal (AIPlayer p, Goal g, float w, AIUnit u) {
this(p,g,w);
addUnit(u);
}
/**
* Determines whether this goal is finished.
* If it is, this means it can be cancelled by its parent.
*
* @return true, if the goal is finished, false otherwise
*/
public boolean isFinished() {
return isFinished;
}
/**
* Cancels a goal and all of its subgoals.
* If a goal is cancelled, it will recursively cancelGoal() its subgoals first,
* and return all units to the object calling this.
* After this method has been called, it should be safe for the parent
* to remove this goal from its list of subgoals.
* </p><p>
* NOTE: Preferably, only the direct parent should call this.
*
* @return A list of all {@link AIUnit} being freed up by this action
*/
public List<AIUnit> cancelGoal() {
logger.finest("Entering method cancelGoal() for "+getDebugDescription());
List<AIUnit> cancelledUnitsList = new ArrayList<AIUnit>();
//get units from subgoals
Iterator<Goal> git = getSubGoalIterator();
while (git!=null && git.hasNext()) {
Goal g = git.next();
List<AIUnit> ulist = g.cancelGoal();
cancelledUnitsList.addAll(ulist);
}
//get own units
Iterator<AIUnit> uit = getOwnedAIUnitsIterator();
while (uit.hasNext()) {
AIUnit u = uit.next();
cancelledUnitsList.add(u);
}
logger.info("Got "+cancelledUnitsList.size()+" units from cancelled subgoals");
return cancelledUnitsList;
}
/**
* Recursively calls {@link #doPlanning} in subgoals that {@link #needsPlanning()},
* then calls its own planning method.
*/
public void doPlanning() {
logger.finest("Entering method doPlanning() for "+getDebugDescription());
boolean subgoalsPlanned = false;
normalizeSubGoalWeights();
Iterator<Goal> git = getSubGoalIterator();
while (git!=null && git.hasNext()) {
Goal g = git.next();
if (g.needsPlanning()) {
g.doPlanning();
subgoalsPlanned = true;
}
}
//after all subgoals have been planned, let's plan ourselves
if (needsPlanning || subgoalsPlanned) {
plan();
needsPlanning = false;
}
}
/**
* Determines whether this or a subgoal {@link #needsPlanning}.
*
* @return true if this Goal or at least one subgoal needs planning, false otherwise
*/
public boolean needsPlanning() {
logger.finest("Entering method needsPlanning() for "+getDebugDescription());
if (needsPlanning) {
return true;
} else {
Iterator<Goal> git = getSubGoalIterator();
while (git!=null && git.hasNext()) {
Goal g = git.next();
if (g.needsPlanning()) {
return true;
}
}
}
return false;
}
/**
* Sets the {@link #needsPlanning} status of this Goal and all its subgoals.
* Should be called by the {@link AIPlayer} once for each of its subgoals
* at the start of a turn. The Goal will handle all other instances of this
* flag needing to be reset internally.
*
* @param p Boolean determining whether to set needsPlanning =true or =false
*/
public void setNeedsPlanningRecursive(boolean p) {
logger.finest("Entering method setNeedsPlanningRecursive() for "+getDebugDescription());
needsPlanning = p;
Iterator<Goal> git = getSubGoalIterator();
while (git!=null && git.hasNext()) {
Goal g = git.next();
g.setNeedsPlanningRecursive(p);
}
}
/**
* Returns the relativeWeight this goal has been weighted with by its parent.
* </p><p>
* NOTE: In many cases, you will want to use {@link #getAbsoluteWeight()} instead.
*
* @return The relative weight {@link #relativeWeight} of this goal
*/
public float getWeight() {
return relativeWeight;
}
/**
* Gets the weight of the parent goal, or 1 if there is no parent goal.
*
* @return The absolute weight [0;1] of the parent goal, or 1 if a parent goal does not exist
*/
public float getParentWeight() {
if (parentGoal == null) {
//we must be a direct goal of our AIPlayer
return 1.0f;
} else {
return parentGoal.getAbsoluteWeight();
}
}
/**
* Returns the absolute weight of this goal.
* </p><p>
* The absolute weight is the weight of this goal in relation to the {@link AIPlayer}.
* This is used when making requests, to allow the AIPlayer to find the
* "most important" request.
*
* @return The absolute weight [0;1] of this goal
*/
public float getAbsoluteWeight() {
return getParentWeight() * relativeWeight;
}
/**
* Sets a relative weight for this goal.
* Each Goal is weighted by its parent goal.
* The parent should assure that the sum of weights given to its subgoals is =1
*
* @param w A relative weight, should be in range [0;1]
*/
public void setWeight(float w) {
relativeWeight = w;
}
/**
* Calling this ensures that the relative weights given to
* subgoals add up to 1.
* </p><p>
* NOTE: This allows for a small margin of error (+/- 0.05),
* to avoid recalculating too often.
*/
public void normalizeSubGoalWeights() {
float sumWeights = 0f;
Iterator<Goal> git = getSubGoalIterator();
while (git!=null && git.hasNext()) {
Goal g = git.next();
sumWeights += g.getWeight();
}
//allow for a small rounding or other error margin before normalizing
if (sumWeights>0f && (sumWeights<0.95f || sumWeights>1.05f)) {
git = getSubGoalIterator();
while (git!=null && git.hasNext()) {
Goal g = git.next();
g.setWeight(g.getWeight()/sumWeights);
}
}
}
// /**
// * Wrapper method for a unit request sent to the {@link AIPlayer}.
// * </p><p>
// * Each Goal can request necessary units from the AIPlayer.
// * Here, such a request is wrapped in a private method for convenience.
// * Each request contains a weight, which is {@link #getAbsoluteWeight()}
// * of this goal, and the number of turns since a unit request has last been granted.
// * The latter should be taken into account as a "bonus weight" by the AIPlayer.
// * </p><p>
// * TODO:Should that be role, instead or alternatively?
// * </p><p>
// * TODO: {@link AIPlayer#addUnitWish(Goal,UnitType,float,int)}; should add
// * requests to a set-like structure, so that there's only one active request
// * per Goal at any time. Since fulfilling a request using {@link #addUnit(AIUnit)}
// * means that {@link #plan()} will be called again during the turn,
// * the Goal will be able to request again.
// *
// * @param ut The {@link UnitType} we'd like to request
// */
// protected void requestUnit(UnitType ut) {
// int turnsWithoutUnit = getGame().getTurn().getNumber() - turnLastUnitAdded;
//
// //TODO: Uncomment after AIPlayer.addUnitWish() has been written.
// //player.addUnitWish(this, ut, getAbsoluteWeight(), turnsWithoutUnit);
// }
/**
* Wrapper method for a worker request sent to the {@link AIPlayer}.
* </p><p>
* Each Goal can request necessary units from the AIPlayer.
* Here, such a request is wrapped in a private method for convenience.
* Each request contains a weight, which is {@link #getAbsoluteWeight()}
* of this goal, and the number of turns since a unit request has last been granted.
* The latter should be taken into account as a "bonus weight" by the AIPlayer.
* </p><p>
* TODO: AIPlayer#addUnitWish(Goal,GoodsType,int,float,int); should add
* requests to a set-like structure, so that there's only one active request
* per Goal at any time. Since fulfilling a request using {@link #addUnit(AIUnit)}
* means that {@link #plan()} will be called again during the turn,
* the Goal will be able to request again.
*
* @param gt The {@link GoodsType} we're requesting a worker for.
* @param minProduction The minimum a unit needs to produce to be considered.
*/
protected void requestWorker(GoodsType gt, int minProduction) {
//TODO: Uncomment after AIPlayer.addWorkerWish() has been written.
//int turnsWithoutUnit = getGame().getTurn().getNumber() - turnLastUnitAdded;
//player.addWorkerWish(this, gt, minProduction, getAbsoluteWeight(), turnsWithoutUnit);
}
/**
* Adds a unit to this goal.
* This may be from {@link AIPlayer} fulfilling a unit request,
* by the parent goal, or by a subgoal that no longer needs the unit.
* </p><p>
* Possible TODO: If the unit we're requesting is a high-interest one,
* such as a Galleon, AIUnit#setLoaningGoal() may be used to
* signal that this unit may _only_ be moved to subgoals, or back to
* {@link AIPlayer}, but not further up the hierarchy or to any other requesting Goal.
*
* @param u The {@link AIUnit} being added to this goal
*/
public void addUnit(AIUnit u) {
logger.finest("Entering method addUnit() for "+getDebugDescription()+" with unit: "+u.getId());
getGame().getTurn().getNumber();
availableUnitsList.add(u);
u.setGoal(this);
needsPlanning = true; //adding a unit to the Goal means it might need planning
isFinished = false; //in case the goal was finished but not yet cancelled
}
/**
* Adds a unit to the parent goal.
* If this goal doesn't have a parent goal,
* the unit will be added to {@link AIPlayer} instead.
*
* @param u The {@link AIUnit} to be added to the parent
*/
protected void addUnitToParent(AIUnit u) {
logger.finest("Entering method addUnitToParent() for "+getDebugDescription()+" with unit: "+u.getId());
if (parentGoal != null) {
parentGoal.addUnit(u);
} else {
//Setting goal=null will make the unit appear in the unit iterator next turn.
//TODO: What about this turn?
u.setGoal(null);
}
}
/**
* Used by a parent goal to check whether this goal, including subgoals,
* can yield a specific unit.
* </p><p>
* This recursively checks its subgoals, if there's no match among the own units.
* </p><p>
* Possible TODO: Check whether AIUnit#isOnLoan() - in which case, we mustn't
* yield a unit unless it's the {@link AIPlayer} that requests.
*
* @param ut The {@link UnitType} wanted by the parent
* @param o The {@link AIObject} (should be AIPlayer or another Goal) calling this
* @return true if this goal or one of its subgoals can yield the specified {@link UnitType}, false otherwise
*/
public boolean canYieldUnit(UnitType ut, AIObject o) {
Iterator<AIUnit> uit = getOwnedAIUnitsIterator();
while (uit.hasNext()) {
AIUnit u = uit.next();
//first found unit is enough
if (u.getUnit().getType().equals(ut)) {
return true;
}
}
//None found among our own units, check subgoals
Iterator<Goal> git = getSubGoalIterator();
while (git!=null && git.hasNext()) {
Goal g = git.next();
if (g.canYieldUnit(ut, o)) {
return true;
}
}
//None found among subgoals
return false;
}
/**
* Returns the absolute weight of the unit which would be yielded by {@link #yieldUnit(UnitType,AIObject)}.
* This is the same as {@link #getAbsoluteWeight()} of the yielding goal.
*
* @param ut The {@link UnitType} wanted by the parent
* @param o The {@link AIObject} (should be AIPlayer or another Goal) calling this
* @return The absolute weight ([0;1]) of the goal currently owning
* the unit that would be yielded.
* Note that the returned value might be 99f if there is no unit to yield.
* The calling function should use {@link #canYieldUnit(UnitType,AIObject)} first,
* or is responsible to sanitize this result itself before trying
* to {@link #yieldUnit(UnitType,AIObject)} based on it.
*/
public float getYieldedUnitWeight(UnitType ut, AIObject o) {
//weights should normally be in range [0;1]
//if there is a matching unit, this will be overwritten
float unitWeight = 99f;
Iterator<AIUnit> uit = getOwnedAIUnitsIterator();
while (uit.hasNext()) {
AIUnit u = uit.next();
//all units in one goal have the same weight, so no need to compare
if (u.getUnit().getType().equals(ut)) {
unitWeight = getAbsoluteWeight();
}
}
//check subgoals
Iterator<Goal> git = getSubGoalIterator();
while (git!=null && git.hasNext()) {
Goal g = git.next();
float newWeight = g.getYieldedUnitWeight(ut, o);
if (newWeight < unitWeight) {
unitWeight = newWeight;
}
}
return unitWeight;
}
/**
* Removes a unit from the goal, potentially from a subgoal,
* and yields it to the caller.
* </p><p>
* Returned unit should be the one with minimum absolute weight,
* see {@link #getYieldedUnitWeight(UnitType,AIObject)}.
*
* @param ut The {@link UnitType} wanted by the parent
* @param o The {@link AIObject} (should be AIPlayer or another Goal) calling this
* @return The {@link AIUnit} with minimal absolute weight.
* Note that this may be null if no matching unit is found!
*/
public AIUnit yieldUnit(UnitType ut, AIObject o) {
float unitWeight = 99f;
AIUnit yieldedUnit = null;
boolean isOwnUnit = false;
Iterator<AIUnit> uit = getOwnedAIUnitsIterator();
while (uit.hasNext()) {
AIUnit u = uit.next();
//all units in one goal have the same weight, so no need to compare
if (u.getUnit().getType().equals(ut)) {
unitWeight = getAbsoluteWeight();
yieldedUnit = u;
isOwnUnit = true;
}
}
//check subgoals
Iterator<Goal> git = getSubGoalIterator();
while (git!=null && git.hasNext()) {
Goal g = git.next();
float newWeight = g.getYieldedUnitWeight(ut, o);
if (newWeight < unitWeight) {
unitWeight = newWeight;
yieldedUnit = g.yieldUnit(ut, o);
isOwnUnit = false;
}
}
if (isOwnUnit) {
removeUnit(yieldedUnit);
needsPlanning = true;
}
return yieldedUnit;
}
/**
* Checks all owned AIUnits for validity, and removes invalid ones.
* An AIUnit is supposed to be invalid if it no longer contains a valid Unit.
* This may be the case if the Unit has been removed from the game between turns.
* </p><p>
* NOTE: The assumption here is that AIUnit#isValid() will return true
* as long as the {@link net.sf.freecol.common.model.Unit} wrapped in it exists.
*/
protected void validateOwnedUnits() {
Iterator<AIUnit> uit = getOwnedAIUnitsIterator();
while (uit.hasNext()) {
AIUnit u = uit.next();
if (!(u.getGoal()==this)) {
logger.warning("Goal "+ getGoalDescription() + " owns unit with another goal: "
+ u.getGoal().getGoalDescription());
removeUnit(u);
}
//TODO: Uncomment after AIUnit.isValid() has been written.
//if (!u.isValid()) {
// removeUnit(u);
//}
}
}
/**
* Returns a string describing just this goal.
* An implementing class may override this method to add specialized information.
* Used by {@link #getDebugDescription()}.
*
* @return a string describing this goal
*/
public String getGoalDescription() {
String goalName = getClass().toString();
goalName = goalName.substring(goalName.lastIndexOf('.') + 1,goalName.length()-4);
return goalName;
}
/**
* Build and return a string describing this goal including its parent goal.
* Used by "Display AI-missions" in debug mode.
*
* @return a string describing this goal
*/
public String getDebugDescription() {
String descr = "";
//if goal has parent goal, add that as well
//no recursive call, to avoid lengthy descriptions
if (parentGoal!=null) {
descr = parentGoal.getGoalDescription() + ">>";
}
descr += getGoalDescription();
return descr;
}
/**
* Returns the tag name of the root element representing this object.
*
* @return "aiGoal"
*/
public static String getXMLElementTagName() {
return "aiGoal";
}
/* INTERFACE ******************************************************************/
/**
* Since internal implementation details may vary,
* each Goal will define an iterator over all of its units.
*
* @return An Iterator over all {@link AIUnit} currently managed by this goal.
*/
protected abstract Iterator<AIUnit> getOwnedAIUnitsIterator();
/**
* Since internal implementation details may vary,
* each Goal will define an iterator over all of its subgoals.
*
* @return An Iterator over all currently existing subgoals.
*/
protected abstract Iterator<Goal> getSubGoalIterator();
/**
* Ensures that a unit moved to another Goal is properly removed from this.
* If a unit is removed from this goal via {@link #yieldUnit(UnitType,AIObject)},
* or if the Unit contained in the {@link AIUnit} no longer is valid,
* this method is called to clean up any remaining references to the unit.
* </p><p>
* Any implementation of this will need to iterate over all AIUnit object
* references used by this goal, and remove those that equal the given parameter.
*
* @param u The AIUnit supposed to be removed from this goal.
*/
protected abstract void removeUnit(AIUnit u);
/**
* This is the method that actually does the planning for this goal.
*
* It should contain:
* <ul>
* <li>calling {@link #validateOwnedUnits()} to remove AIUnits no longer
* containing a valid unit</li>
* <li>putting units on the {@link #availableUnitsList} to work
* <ul><li>eventually by adding it to one of the subgoals, or</li>
* <li>by adding it back to the {@link AIPlayer}, or</li>
* <li>last but not least, by spending their movement points for some internal mission</li></ul></li>
* <li>requesting new units (via a method like {@link #requestWorker(GoodsType,int)})</li>
* <li>managing direct subgoals, including:
* <ul><li>creating new ones, if necessary</li>
* <li>cancelling those with isFinished()==true</li>
* <li>setting new weights using {@link #setWeight(float)}</li></ul></li>
* <li>setting our own isFinished status</li>
* </ul>
*/
protected abstract void plan();
/**
* Writes this object to an XML stream.
*
* @param out The target stream.
* @throws XMLStreamException if there are any problems writing to the
* stream.
*/
protected abstract void toXMLImpl(XMLStreamWriter out) throws XMLStreamException;
/**
* 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.
*/
protected abstract void readFromXMLImpl(XMLStreamReader in) throws XMLStreamException;
/* How this is supposed to work:
*
* Assuming the AIPlayer has a set of goals...
* At the start of a turn, it will call
*
* setNeedsPlanningRecursive(true);
*
* for all its goals. This will set all goals in a state of needing a check.
* After that, it will iterate through its goals in a WHILE-loop:
*
* boolean furtherPlanning = true;
* while (furtherPlanning) {
* FOR ALL GOALS g DO {
* g.doPlanning();
* }
*
* SATISFY_UNIT_REQUESTS();
* HANDLE_REMAINING_REQUESTS();
*
* furtherPlanning = false;
* FOR ALL GOALS g DO {
* furtherPlanning = (furtherPlanning || g.needsPlanning());
* }
* }
*
* The first FOR-loop will recursively reach all existing goals and plan()
* each of them once, bottom-up.
*
* After that, unit requests from the goals will have piled up.
* SATISFY_UNIT_REQUESTS() will try to satisfy as many as possible,
* in order of importance, by using the units that were created between turns
* or that have been given back by other goals.
*
* HANDLE_REMAINING_REQUESTS() will try to do something about remaining requests,
* for example by buying/building new units, setting up new goals etc.
*
* This, as well as goals called later during this process moving a unit to a former goal,
* may have set one or more of the goals to needsPlanning=true. In the second FOR-loop,
* we check whether this is the case, to eventually repeat the process.
*
* Eventually, the WHILE-loop will exit. Any units in the goals will have
* been used for this turn. There may still be some excess units that haven't been
* requested by any Goal. The AIPlayer may now choose to create a new Goal for these,
* add them to an existing Goal (which will make use of them next turn),
* or just keep them unused to have something to deal out next turn.
*/
}