/** * 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.common.model; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Locale; import java.util.Map.Entry; import java.util.Random; import java.util.Set; import java.util.logging.Logger; import javax.xml.stream.XMLStreamConstants; import javax.xml.stream.XMLStreamException; import javax.xml.stream.XMLStreamReader; import javax.xml.stream.XMLStreamWriter; import net.sf.freecol.client.gui.i18n.Messages; import net.sf.freecol.common.model.Map.Position; import net.sf.freecol.common.model.NationOptions.NationState; import net.sf.freecol.common.model.Region.RegionType; import net.sf.freecol.common.option.OptionGroup; import org.w3c.dom.Element; /** * Represents a player. The player can be either a human player or an * AI-player. * * In addition to storing the name, nation e.t.c. of the player, it also stores * various defaults for the player. One example of this is the * {@link #getEntryLocation entry location}. */ public class Player extends FreeColGameObject implements Nameable { private static final Logger logger = Logger.getLogger(Player.class.getName()); // A magic constant to denote that a players gold is not tracked. public static final int GOLD_NOT_ACCOUNTED = Integer.MIN_VALUE; public static final int SCORE_SETTLEMENT_DESTROYED = -40; public static final String ASSIGN_SETTLEMENT_NAME = ""; /** * The XML tag name for the set of founding fathers. */ private static final String FOUNDING_FATHER_TAG = "foundingFathers"; /** * The XML tag name for the set of offered founding fathers. */ private static final String OFFERED_FATHER_TAG = "offeredFathers"; /** * The XML tag name for the stance array. */ private static final String STANCE_TAG = "stance"; /** * The XML tag name for the tension array. */ private static final String TENSION_TAG = "tension"; /** * The name of the unknown enemy. */ public static final String UNKNOWN_ENEMY = "unknown enemy"; /** * Constants for describing the stance towards a player. */ public static enum Stance { // Valid transitions: // // [FROM] \ [TO] U A P C W Reasons // ---------------------------------- a = attack // UNCONTACTED | - x c x i | c = contact // ALLIANCE | x - d x adit | d = act of diplomacy // PEACE | x d - x adit | i = incitement/rebellion // CEASE_FIRE | x d t - adit | t = change of tension // WAR | x d ds dt - | s = surrender // ---------------------------------- x = invalid // UNCONTACTED, ALLIANCE, PEACE, CEASE_FIRE, WAR; // Helpers to enforce valid transitions private void badStance() throws IllegalStateException { throw new IllegalStateException("Bogus stance"); } private void badTransition(Stance newStance) throws IllegalStateException { throw new IllegalStateException("Bad transition: " + toString() + " -> " + newStance.toString()); } /** * Check whether tension has changed enough to merit a stance * change. Do not simply check for crossing tension * thresholds, add in Tension.DELTA to provide a bit of * hysteresis to dampen ringing. * * @param tension The <code>Tension</code> to check. * @return The <code>Stance</code> appropriate to the tension level. */ public Stance getStanceFromTension(Tension tension) { int value = tension.getValue(); switch (this) { case WAR: // Cease fire if tension decreases if (value <= Tension.Level.CONTENT.getLimit()-Tension.DELTA) { return Stance.CEASE_FIRE; } break; case CEASE_FIRE: // Peace if tension decreases if (value <= Tension.Level.HAPPY.getLimit()-Tension.DELTA) { return Stance.PEACE; } // Fall through case ALLIANCE: case PEACE: // War if tension increases if (value > Tension.Level.HATEFUL.getLimit()+Tension.DELTA) { return Stance.WAR; } break; case UNCONTACTED: break; default: this.badStance(); } return this; } /** * A stance change is about to happen. Get the appropriate tension * modifier. * * @param newStance The new <code>Stance</code>. * @return A modifier to the current tension. */ public int getTensionModifier(Stance newStance) throws IllegalStateException { switch (newStance) { case UNCONTACTED: badTransition(newStance); case ALLIANCE: switch (this) { case UNCONTACTED: badTransition(newStance); case ALLIANCE: return 0; case PEACE: return Tension.ALLIANCE_MODIFIER; case CEASE_FIRE: return Tension.ALLIANCE_MODIFIER + Tension.PEACE_TREATY_MODIFIER; case WAR: return Tension.ALLIANCE_MODIFIER + Tension.CEASE_FIRE_MODIFIER + Tension.PEACE_TREATY_MODIFIER; default: this.badStance(); } case PEACE: switch (this) { case UNCONTACTED: return Tension.CONTACT_MODIFIER; case ALLIANCE: return Tension.DROP_ALLIANCE_MODIFIER; case PEACE: return 0; case CEASE_FIRE: return Tension.PEACE_TREATY_MODIFIER; case WAR: return Tension.CEASE_FIRE_MODIFIER + Tension.PEACE_TREATY_MODIFIER; default: this.badStance(); } case CEASE_FIRE: switch (this) { case UNCONTACTED: badTransition(newStance); case ALLIANCE: badTransition(newStance); case PEACE: badTransition(newStance); case CEASE_FIRE: return 0; case WAR: return Tension.CEASE_FIRE_MODIFIER; default: this.badStance(); } case WAR: switch (this) { case UNCONTACTED: return Tension.WAR_MODIFIER; case ALLIANCE: return Tension.WAR_MODIFIER; case PEACE: return Tension.WAR_MODIFIER; case CEASE_FIRE: return Tension.RESUME_WAR_MODIFIER; case WAR: return 0; default: this.badStance(); } default: throw new IllegalStateException("Bogus newStance"); } } } /** * Only used by AI - stores the tension levels, 0-1000 with 1000 maximum * hostility. */ protected java.util.Map<Player, Tension> tension = new HashMap<Player, Tension>(); // TODO: move this to AIPlayer /** * Stores the stance towards the other players. One of: WAR, CEASE_FIRE, * PEACE and ALLIANCE. */ protected java.util.Map<String, Stance> stance = new HashMap<String, Stance>(); /** * The name of this player. This defaults to the user name in case of a * human player and the rulerName of the NationType in case of an AI player. */ protected String name; /** The name of this player as an independent nation. */ protected String independentNationName; /** The NationType of this player. */ protected NationType nationType; /** The nation ID of this player, e.g. "model.nation.dutch". */ protected String nationID; /** The name this player uses for the New World. */ protected String newLandName = null; /** Is this player an admin? */ protected boolean admin; /** The current score of this player. */ protected int score; /** The amount of gold this player owns. */ protected int gold; /** * The number of immigration points. Immigration points are an * abstract game concept. They are generated by but are not * identical to crosses. */ protected int immigration; /** * The number of liberty points. Liberty points are an * abstract game concept. They are generated by but are not * identical to bells. */ protected int liberty; /** * The number of liberty bells produced towards the intervention * force. */ protected int interventionBells; /** The market for Europe. */ protected Market market; /** The European port/location for this player. */ protected Europe europe; /** The monarch for this player. */ protected Monarch monarch; protected boolean ready; /** True if this is an AI player. */ protected boolean ai; /** True if player has been attacked by privateers. */ protected boolean attackedByPrivateers = false; /** SoL from last turn. */ protected int oldSoL; /** Is this player dead? */ protected boolean dead = false; /** The founding fathers in this Player's congress. */ final protected Set<FoundingFather> allFathers = new HashSet<FoundingFather>(); /** Current founding father being recruited. */ protected FoundingFather currentFather; /** The offered founding fathers. */ final protected List<FoundingFather> offeredFathers = new ArrayList<FoundingFather>(); /** The current tax rate for this player. */ protected int tax = 0; /** The type of player. */ public static enum PlayerType { NATIVE, COLONIAL, REBEL, INDEPENDENT, ROYAL, UNDEAD } protected PlayerType playerType; protected int immigrationRequired = 12; protected Location entryLocation; /** The Units this player owns. */ protected final java.util.Map<String, Unit> units = new HashMap<String, Unit>(); /** The Settlements this player owns. */ protected final List<Settlement> settlements = new ArrayList<Settlement>(); /** Trade routes of this player. */ protected final List<TradeRoute> tradeRoutes = new ArrayList<TradeRoute>(); /** Model messages for this player. */ protected final List<ModelMessage> modelMessages = new ArrayList<ModelMessage>(); /** The history events occuring with this player. */ protected final List<HistoryEvent> history = new ArrayList<HistoryEvent>(); /** The last-sale data. */ protected HashMap<String, LastSale> lastSales = null; /** Indices of largest used region name by type. */ protected final HashMap<String, Integer> nameIndex = new HashMap<String, Integer>(); // Temporary variables: // Tiles the player can see. // No access to canSeeTiles without taking canSeeLock. private boolean[][] canSeeTiles = null; private final Object canSeeLock = new Object(); /** * Whether the player is bankrupt, i.e. unable to pay for the * maintenance of all buildings. */ private boolean bankrupt; // Contains the abilities and modifiers of this type. protected final FeatureContainer featureContainer = new FeatureContainer(); // Maximum food consumption of unit types available to this player. private int maximumFoodConsumption = -1; private final UnitIterator nextActiveUnitIterator = new UnitIterator(this, new ActivePredicate(this)); private final UnitIterator nextGoingToUnitIterator = new UnitIterator(this, new GoingToPredicate(this)); /** * The HighSeas is a Location that enables Units to travel between * the New World and one or several European Ports. */ protected HighSeas highSeas; /** * A cache of settlement names, a capital for natives, and a fallback * settlement name prefix. * Does not need to be serialized. */ protected List<String> settlementNames = null; protected String capitalName = null; /** * A cache of ship names, including a fallback ship name prefix. * Does not need to be serialized. */ protected List<String> shipNames = null; protected String shipFallback = null; public static final Comparator<Player> playerComparator = new Comparator<Player>() { public int compare(Player player1, Player player2) { int counter1 = 0; int counter2 = 0; if (player1.isAdmin()) { counter1 += 8; } if (!player1.isAI()) { counter1 += 4; } if (player1.isEuropean()) { counter1 += 2; } if (player2.isAdmin()) { counter2 += 8; } if (!player2.isAI()) { counter2 += 4; } if (player2.isEuropean()) { counter2 += 2; } return counter2 - counter1; } }; /** * Constructor for ServerPlayer. */ protected Player() { // empty } /** * Constructor for ServerPlayer. */ protected Player(Game game) { super(game); } /** * * Initiates a new <code>Player</code> from an <code>Element</code> and * registers this <code>Player</code> at the specified game. * * @param game The <code>Game</code> this object belongs to. * @param in The input stream containing the XML. * @throws XMLStreamException if a problem was encountered during parsing. */ public Player(Game game, XMLStreamReader in) throws XMLStreamException { super(game, in); readFromXML(in); } /** * Initiates a new <code>Player</code> from an <code>Element</code> and * registers this <code>Player</code> at the specified game. * * @param game The <code>Game</code> this object belongs to. * @param e An XML-element that will be used to initialize this object. * */ public Player(Game game, Element e) { super(game, e); readFromXMLElement(e); } /** * Initiates a new <code>Player</code> with the given ID. The object * should later be initialized by calling either {@link * #readFromXML(XMLStreamReader)} or {@link #readFromXMLElement(Element)}. * * @param game The <code>Game</code> in which this object belong. * @param id The unique identifier for this object. */ public Player(Game game, String id) { super(game, id); } /** * Get this settlement's feature container. * * @return The <code>FeatureContainer</code>. */ @Override public final FeatureContainer getFeatureContainer() { return featureContainer; } /** * Adds a <code>ModelMessage</code> for this player. * * @param modelMessage The <code>ModelMessage</code>. */ public void addModelMessage(ModelMessage modelMessage) { modelMessage.setOwnerId(getId()); modelMessages.add(modelMessage); } /** * Returns all ModelMessages for this player. * * @return all ModelMessages for this player. */ public List<ModelMessage> getModelMessages() { return modelMessages; } /** * Returns all new ModelMessages for this player. * * @return all new ModelMessages for this player. */ public List<ModelMessage> getNewModelMessages() { ArrayList<ModelMessage> out = new ArrayList<ModelMessage>(); for (ModelMessage message : modelMessages) { if (message.hasBeenDisplayed()) { continue; } else { out.add(message); // preserve message order } } return out; } /** * Refilters the current model messages, removing the ones that * are no longer valid. * * @param options The <code>OptionGroup</code> for message display * to enforce. */ public void refilterModelMessages(OptionGroup options) { for (ModelMessage m : new ArrayList<ModelMessage>(modelMessages)) { try { String id = m.getMessageType().getOptionName(); if (!options.getBoolean(id)) modelMessages.remove(m); } catch (Exception e) {}; } } /** * Removes all undisplayed model messages for this player. */ public void removeModelMessages() { Iterator<ModelMessage> messageIterator = modelMessages.iterator(); while (messageIterator.hasNext()) { ModelMessage message = messageIterator.next(); if (message.hasBeenDisplayed()) { messageIterator.remove(); } } } /** * Removes all the model messages for this player. */ public void clearModelMessages() { modelMessages.clear(); } /** * Sometimes an event causes the source (and display) fields in an * accumulated model message to become invalid (e.g. Europe disappears * on independence). This routine is for cleaning up such cases. * * @param source the source field that has become invalid * @param newSource a new source field to replace the old with, or * if null then remove the message */ public void divertModelMessages(FreeColGameObject source, FreeColGameObject newSource) { // Since we are changing the list, we need to copy it to be able // to iterate through it List<ModelMessage> modelMessagesList = new ArrayList<ModelMessage>(); modelMessagesList.addAll(modelMessages); for (ModelMessage modelMessage : modelMessagesList) { if (modelMessage.getSourceId() == source.getId()) { if (newSource == null) { modelMessages.remove(modelMessage); } else { modelMessage.divert(newSource); } } } } /** * Returns the maximum food consumption of any unit types * available to this player. * * @return an <code>int</code> value */ public int getMaximumFoodConsumption() { if (maximumFoodConsumption < 0) { for (UnitType unitType : getSpecification().getUnitTypeList()) { if (unitType.isAvailableTo(this)) { int foodConsumption = 0; for (GoodsType foodType : getSpecification().getFoodGoodsTypeList()) { foodConsumption += unitType.getConsumptionOf(foodType); } if (foodConsumption > maximumFoodConsumption) { maximumFoodConsumption = foodConsumption; } } } } return maximumFoodConsumption; } /** * Returns the current score of the player. * * @return an <code>int</code> value */ public int getScore() { return score; } /** * Set the current score of the player. * * @param newScore The new score. */ public void setScore(int newScore) { score = newScore; } /** * Modifies the score of the player by the given value. * * @param value an <code>int</code> value */ public void modifyScore(int value) { score += value; } /** * Returns this Player's Market. * * @return This Player's Market. */ public Market getMarket() { return market; } /** * Resets this Player's Market. */ public void reinitialiseMarket() { market = new Market(getGame(), this); } /** * What is the name of the player's market? * Following a declaration of independence we are assumed to trade * broadly with any European market rather than a specific port. * * @return A name for the player's market. */ public StringTemplate getMarketName() { return (getEurope() == null) ? StringTemplate.key("model.market.independent") : StringTemplate.key(nationID + ".europe"); } /** * Checks if this player owns the given <code>Settlement</code>. * * @param s The <code>Settlement</code>. * @return <code>true</code> if this <code>Player</code> owns the given * <code>Settlement</code>. */ public boolean hasSettlement(Settlement s) { return settlements.contains(s); } /** * Adds a given settlement to this player's list of settlements. * * @param settlement The <code>Settlement</code> to add. */ public void addSettlement(Settlement settlement) { if (!hasSettlement(settlement)) { if (settlement.getOwner() != this) { throw new IllegalStateException("Player does not own settlement."); } settlements.add(settlement); } } /** * Removes the given settlement from this player's list of settlements. * * @param settlement The <code>Settlement</code> to remove. * @return True if the settlement was removed. */ public boolean removeSettlement(Settlement settlement) { return settlements.remove(settlement); } public boolean owns(Ownable ownable) { if (ownable == null) return false; return this.equals(ownable.getOwner()); } /** * Returns a list of all Settlements this player owns. * * @return The settlements this player owns. */ public List<Settlement> getSettlements() { return settlements; } /** * Get the number of settlements. * * @return The number of settlements this player has. */ public int getNumberOfSettlements() { return settlements.size(); } /** * Gets a fresh list of all colonies this player owns. * * @return A fresh list of the colonies this player owns. */ public List<Colony> getColonies() { ArrayList<Colony> colonies = new ArrayList<Colony>(); for (Settlement s : settlements) { if (s instanceof Colony) { colonies.add((Colony) s); } else { throw new RuntimeException("getColonies can only be called for players whose settlements are colonies."); } } return colonies; } /** * Returns a sorted list of all Colonies this player owns. * * @param c A comparator to operate on the colony list. * @return A fresh list of the colonies this player owns. */ public List<Colony> getSortedColonies(Comparator<Colony> c) { List<Colony> colonies = getColonies(); Collections.sort(colonies, c); return colonies; } /** * Returns the sum of units currently working in the colonies of * this player. * * @return Sum of units currently working in the colonies. */ public int getColoniesPopulation() { int i = 0; for (Colony c : getColonies()) { i += c.getUnitCount(); } return i; } /** * Gets the score by which we decide the weakest and strongest AI * players for the Spanish Succession event. * * @return A strength score. */ public int getSpanishSuccessionScore() { // TODO: try getColoniesPopulation. return getScore(); } /** * Returns the <code>Colony</code> with the given name. * * @param name The name of the <code>Colony</code>. * @return The <code>Colony</code> or <code>null</code> if this player * does not have a <code>Colony</code> with the specified name. */ public Colony getColony(String name) { for (Colony colony : getColonies()) { if (colony.getName().equals(name)) { return colony; } } return null; } /** * Returns a list of all IndianSettlements this player owns. * * @return The indian settlements this player owns. */ public List<IndianSettlement> getIndianSettlements() { ArrayList<IndianSettlement> indianSettlements = new ArrayList<IndianSettlement>(); for (Settlement s : settlements) { if (s instanceof IndianSettlement) { indianSettlements.add((IndianSettlement) s); } else { throw new RuntimeException("getIndianSettlements found: " + s); } } return indianSettlements; } /** * Returns the <code>IndianSettlement</code> with the given name. * * @param name The name of the <code>IndianSettlement</code>. * @return The <code>IndianSettlement</code> or <code>null</code> if this player * does not have a <code>IndianSettlement</code> with the specified name. */ public IndianSettlement getIndianSettlement(String name) { for (IndianSettlement settlement : getIndianSettlements()) { if (settlement.getName().equals(name)) { return settlement; } } return null; } /** * Returns a list of all IndianSettlements this player owns that have * missions, optionally owned by a specific player. * * @param other If non-null, collect only missions established by * this <code>Player</code> * @return The settlements this player owns with the specified * mission type. */ public List<IndianSettlement> getIndianSettlementsWithMission(Player other) { ArrayList<IndianSettlement> indianSettlements = new ArrayList<IndianSettlement>(); for (Settlement s : settlements) { Unit missionary; if (s instanceof IndianSettlement && (missionary = ((IndianSettlement)s).getMissionary()) != null && (other == null || missionary.getOwner() == other)) { indianSettlements.add((IndianSettlement) s); } } return indianSettlements; } /** * Find a <code>Settlement</code> by name. * * @param name The name of the <code>Settlement</code>. * @return The <code>Settlement</code>, or <code>null</code> if not found. **/ public Settlement getSettlement(String name) { return (isIndian()) ? getIndianSettlement(name) : getColony(name); } /** * Installs suitable settlement names (and the capital if native) * into the player name cache. * * @param random An optional pseudo-random number source. */ private void initializeSettlementNames(Random random) { if (settlementNames == null) { settlementNames = new ArrayList<String>(); settlementNames.addAll(Messages.getSettlementNames(this)); if (isIndian()) { capitalName = settlementNames.remove(0); if (random != null) { Collections.shuffle(settlementNames, random); } } else { capitalName = null; } logger.info("Installed " + settlementNames.size() + " settlement names for player " + this); } } /** * Gets the name of this players capital. Only meaningful to natives. * * @param random An optional pseudo-random number source. * @return The name of this players capital. */ public String getCapitalName(Random random) { if (isEuropean()) return null; if (capitalName == null) initializeSettlementNames(random); return capitalName; } /** * Gets a settlement name suitable for this player. * * @param random An optional pseudo-random number source. * @return A new settlement name. */ public String getSettlementName(Random random) { Game game = getGame(); if (settlementNames == null) initializeSettlementNames(random); // Try the names in the players national name list. while (!settlementNames.isEmpty()) { String name = settlementNames.remove(0); if (game.getSettlement(name) == null) return name; } // Fallback method final String base = Messages.message((isEuropean()) ? "Colony" : "Settlement") + "-"; String name; int i = settlements.size() + 1; while (game.getSettlement(name = base + Integer.toString(i)) != null) { i++; } return name; } /** * Installs suitable ships names into the player name cache. * Optionally shuffles all but the first name so so as to provide * a stable starting ship name. * * @param names A list of ship names with the fallback prefix first. * @param random A <code>Random</code> number source. */ private void initializeShipNames(Random random) { if (shipNames == null) { shipNames = new ArrayList<String>(); shipNames.addAll(Messages.getShipNames(this)); shipFallback = (shipNames.isEmpty()) ? null : shipNames.remove(0); String startingShip = (shipNames.isEmpty()) ? null : shipNames.remove(0); if (random != null) { Collections.shuffle(shipNames, random); } if (startingShip != null) shipNames.add(0, startingShip); logger.info("Installed " + shipNames.size() + " ship names for player " + this.toString()); } } /** * Gets a new name for a unit. * Currently only names naval units, not specific to type. * TODO: specific names for types. * * @param type The <code>UnitType</code> to choose a name for. * @param random A pseudo-random number source. * @return A name for the unit, or null if not available. */ public String getUnitName(UnitType type, Random random) { String name; if (!type.isNaval()) return null; // Collect all the names of existing naval units. List<String> navalNames = new ArrayList<String>(); for (Unit u : getUnits()) { if (u.isNaval() && u.getName() != null) { navalNames.add(u.getName()); } } // Find a new name in the installed ship names if possible. if (shipNames == null) initializeShipNames(random); while (!shipNames.isEmpty()) { name = shipNames.remove(0); if (!navalNames.contains(name)) return name; } // Fallback method if (shipFallback != null) { final String base = shipFallback + "-"; int i = 0; while (navalNames.contains(name = base + Integer.toString(i))) i++; return name; } return null; } /** * Returns the type of this player. * * @return The player type. */ public PlayerType getPlayerType() { return playerType; } /** * Sets the player type. * * @param type The new player type. * @see #getPlayerType */ public void setPlayerType(PlayerType type) { playerType = type; } /** * Checks if this player is european. This includes the "Royal Expeditionay * Force". * * @return <i>true</i> if this player is european and <i>false</i> * otherwise. */ public boolean isEuropean() { return nationType != null && nationType.isEuropean(); } /** * Checks if this player is indian. This method returns the opposite of * {@link #isEuropean()}. * * @return <i>true</i> if this player is indian and <i>false</i> * otherwise. */ public boolean isIndian() { return playerType == PlayerType.NATIVE; } /** * Checks if this player is undead. * * @return True if this player is undead. */ public boolean isUndead() { return playerType == PlayerType.UNDEAD; } /** * Checks if this player is a "royal expeditionary force. * * @return <code>true</code> is the given nation is a royal expeditionary * force and <code>false</code> otherwise. */ public boolean isREF() { return nationType != null && nationType.isREF(); } /** * Determines whether this player is an AI player. * * @return Whether this player is an AI player. */ public boolean isAI() { return ai; } /** * Sets whether this player is an AI player. * * @param ai <code>true</code> if this <code>Player</code> is controlled * by the computer. */ public void setAI(boolean ai) { this.ai = ai; } /** * Checks if this player is an admin. * * @return <i>true</i> if the player is an admin and <i>false</i> * otherwise. */ public boolean isAdmin() { return admin; } /** * Checks if this player is dead. A <code>Player</code> dies when it * loses the game. * * @return <code>true</code> if this <code>Player</code> is dead. */ public boolean isDead() { return dead; } /** * Get the player death state. * This is indeed identical to isDead(), but is needed for partial * updates to complement the setDead() function. * * @return True if this <code>Player</code> is dead. */ public boolean getDead() { return dead; } /** * Sets this player to be dead or not. * * @param dead Should be set to <code>true</code> when this * <code>Player</code> dies. * @see #isDead */ public void setDead(boolean dead) { this.dead = dead; } /** * Get the <code>Bankrupt</code> value. * * @return a <code>boolean</code> value */ public final boolean isBankrupt() { return bankrupt; } /** * Set the <code>Bankrupt</code> value. * * @param newBankrupt The new Bankrupt value. */ public final void setBankrupt(final boolean newBankrupt) { this.bankrupt = newBankrupt; } /** * Checks whether this player is at war with any other player. * * @return <i>true</i> if this player is at war with any other. */ public boolean isAtWar() { for (Player player : getGame().getPlayers()) { if (atWarWith(player)) return true; } return false; } /** * Checks if this player has work to do if it is a REF-player. * * @return True if any of our units are located in the new * world or a nation is in rebellion against us. */ public boolean isWorkForREF() { for (Unit u : getUnits()) { // Work to do if unit in the new world if (u.getTile() != null) return true; } return !getRebels().isEmpty(); } /** * Gets a list of the players in rebellion against this (REF) player. * * @return A list of nations in rebellion against us. */ public List<Player> getRebels() { List<Player> rebels = new ArrayList<Player>(); for (Player p : getGame().getLiveEuropeanPlayers()) { if (p.getREFPlayer() == this && (p.getPlayerType() == PlayerType.REBEL || p.getPlayerType() == PlayerType.UNDEAD)) rebels.add(p); } return rebels; } /** * A variety of reasons why a tile can not be claimed, either * to found a settlement or just to be used by one, including the * double negative NONE == "no reason" case. */ public static enum NoClaimReason { NONE, // Actually, tile can be claimed TERRAIN, // Not on settleable terrain RUMOUR, // Europeans can not claim tiles with LCR WATER, // Natives do not claim water SETTLEMENT, // Settlement present WORKED, // One of our settlements is working this tile EUROPEANS, // Owned by Europeans and not for sale NATIVES, // Owned by natives and they want payment for it }; /** * Can a tile be owned by this player? * * @param tile The <code>Tile</code> to consider. * @return True if the tile can be owned by this player. */ public boolean canOwnTile(Tile tile) { return canOwnTileReason(tile) == NoClaimReason.NONE; } /** * Can a tile be owned by this player? * This is a test of basic practicality and does not consider * the full complexity of tile ownership issues. * * @param tile The <code>Tile</code> to consider. * @return The reason why/not the tile can be owned by this player. */ private NoClaimReason canOwnTileReason(Tile tile) { return (isEuropean()) ? ((tile.hasLostCityRumour()) ? NoClaimReason.RUMOUR : NoClaimReason.NONE) : ((tile.isLand()) ? NoClaimReason.NONE : NoClaimReason.WATER); } /** * Checks if a tile can be claimed for use by a settlement. * * @param tile The <code>Tile</code> to try to claim. * @return True if the tile can be claimed to found a settlement. */ public boolean canClaimForSettlement(Tile tile) { return canClaimForSettlementReason(tile) == NoClaimReason.NONE; } /** * The test for whether a tile can be freely claimed by a player * settlement (freely => not by purchase or stealing). The rule * for the center tile is different, see below. * * The tile must be ownable by this player, settlement-free, and * either not currently owned, owned by this player and not by * another settlement that is using the tile, or owned by someone * else who does not want anything for it. Got that? * * @param tile The <code>Tile</code> to try to claim. * @return The reason why/not the tile can be claimed. */ public NoClaimReason canClaimForSettlementReason(Tile tile) { int price; NoClaimReason reason = canOwnTileReason(tile); return (reason != NoClaimReason.NONE) ? reason : (tile.getSettlement() != null) ? NoClaimReason.SETTLEMENT : (tile.getOwner() == null) ? NoClaimReason.NONE : (tile.getOwner() == this) ? ((tile.isInUse()) ? NoClaimReason.WORKED : NoClaimReason.NONE) : ((price = getLandPrice(tile)) < 0) ? NoClaimReason.EUROPEANS : (price > 0) ? NoClaimReason.NATIVES : NoClaimReason.NONE; } /** * Can a tile be claimed to found a settlement on? * * @param tile The <code>Tile</code> to try to claim. * @return True if the tile can be claimed to found a settlement. */ public boolean canClaimToFoundSettlement(Tile tile) { return canClaimToFoundSettlementReason(tile) == NoClaimReason.NONE; } /** * Can a tile be claimed to found a settlement on? * Almost the same as canClaimForSettlement but there is an extra * requirement that the tile be of a settleable type, and some * relaxations that allow free center tile acquisition * * @param tile The <code>Tile</code> to try to claim. * @return The reason why/not the tile can be claimed. */ public NoClaimReason canClaimToFoundSettlementReason(Tile tile) { NoClaimReason reason; return (!tile.getType().canSettle()) ? NoClaimReason.TERRAIN : ((reason = canClaimForSettlementReason(tile)) != NoClaimReason.NATIVES) ? reason : (canClaimFreeCenterTile(tile)) ? NoClaimReason.NONE : NoClaimReason.NATIVES; } /** * Is this tile claimable for a colony center tile under the * special provisions of the model.option.buildOnNativeLand option. * * @param tile The <code>Tile</code> to try to claim. * @return True if the tile can be claimed. */ private boolean canClaimFreeCenterTile(Tile tile) { String build = getGame().getSpecification() .getStringOption("model.option.buildOnNativeLand").getValue(); return isEuropean() && tile.getOwner() != null && tile.getOwner().isIndian() && ("model.option.buildOnNativeLand.always".equals(build) || ("model.option.buildOnNativeLand.first".equals(build) && hasZeroSettlements()) || ("model.option.buildOnNativeLand.firstAndUncontacted".equals(build) && hasZeroSettlements() && (tile.getOwner() == null || tile.getOwner().getStance(this) == Stance.UNCONTACTED))); } /** * The second and third cases of buildOnNative land need to test * if the player has no settlements yet. We can not just check * that the number of settlement is zero because by the time the * settlement is being placed and we are collecting the tiles to * claim, the settlement already exists and thus there will * already be one settlement--- so we have to check if that one * settlement is on the map yet. * * @return True if the player has no settlements (on the map) yet. */ private boolean hasZeroSettlements() { List<Settlement> settlements = getSettlements(); return settlements.isEmpty() || (settlements.size() == 1 && settlements.get(0).getTile().getSettlement() == null); } /** * Can the ownership of this tile be claimed for the purposes of * making an improvement. Quick test that does not handle the * curly case of tile transfer between colonies, or guarantee * success (natives may want to be paid), but just that success is * possible. * * @param tile The <code>Tile</code> to consider. * * @return True if the tile ownership can be claimed. */ public boolean canClaimForImprovement(Tile tile) { Player owner = tile.getOwner(); return owner == null || owner == this || getLandPrice(tile) == 0; } /** * Can a tile be acquired from its owners and used for an improvement? * Slightly weakens canClaimForImprovement to allow for purchase * and/or stealing. * * @param tile The <code>Tile</code> to consider. * @return True if the tile ownership can be claimed. */ public boolean canAcquireForImprovement(Tile tile) { return canClaimForImprovement(tile) || getLandPrice(tile) > 0; } /** * Get the <code>Unit</code> value. * * @return a <code>List<Unit></code> value */ public final Unit getUnit(String id) { return units.get(id); } /** * Set the <code>Unit</code> value. * * @param newUnit The new Units value. */ public final void setUnit(final Unit newUnit) { if (newUnit == null) { logger.warning("Unit to add is null"); return; } // make sure the owner of the unit is set first, before adding it to the list if(newUnit.getOwner() != null && !this.owns(newUnit)){ throw new IllegalStateException(this + " adding another players unit=" + newUnit); } units.put(newUnit.getId(), newUnit); } /** * Remove Unit. * * @param oldUnit an <code>Unit</code> value */ public void removeUnit(final Unit oldUnit) { if (oldUnit != null) { units.remove(oldUnit.getId()); } } /** * Gets the price to this player for a proposed unit. * * @param au The proposed <code>AbstractUnit</code>. * @return The price for the unit. */ public int getPrice(AbstractUnit au) { Specification spec = getSpecification(); UnitType unitType = au.getUnitType(spec); if (unitType.hasPrice()) { int price = getEurope().getUnitPrice(unitType); for (EquipmentType equip : au.getEquipment(spec)) { for (AbstractGoods goods : equip.getGoodsRequired()) { price += getMarket().getBidPrice(goods.getType(), goods.getAmount()); } } return price * au.getNumber(); } else { return INFINITY; } } /** * Gets the total percentage of rebels in all this player's colonies. * * @return The total percentage of rebels in all this player's colonies. */ public int getSoL() { int sum = 0; int number = 0; for (Colony c : getColonies()) { sum += c.getSoL(); number++; } if (number > 0) { return sum / number; } else { return 0; } } /** * Get the <code>IndependentNationName</code> value. * * @return a <code>String</code> value */ public final String getIndependentNationName() { return independentNationName; } /** * Set the <code>IndependentNationName</code> value. * * @param newIndependentNationName The new IndependentNationName value. */ public final void setIndependentNationName(final String newIndependentNationName) { this.independentNationName = newIndependentNationName; } /** * Gets the <code>Player</code> controlling the "Royal Expeditionary * Force" for this player. * * @return The player, or <code>null</code> if this player does not have a * royal expeditionary force. */ public Player getREFPlayer() { Nation ref = getNation().getRefNation(); return (ref == null) ? null : getGame().getPlayer(ref.getId()); } /** * Gets the name this player has chosen for the new land. * * @return The name of the new world as chosen by the <code>Player</code>, * or null if none chosen yet. */ public String getNewLandName() { return newLandName; } /** * Returns true if the player already selected a new name for the discovered * land. * * @return true if the player already set a name for the newly discovered * land, otherwise false. */ public boolean isNewLandNamed() { return newLandName != null; } /** * Sets the name this player uses for the new land. * * @param newLandName This <code>Player</code>'s name for the new world. */ public void setNewLandName(String newLandName) { this.newLandName = newLandName; } /** * Returns the price of the given land. * * @param tile The <code>Tile</code> to get the price for. * @return The price of the land if it is for sale, zero if it is already * ours, unclaimed or unwanted, negative if it is not for sale. */ public int getLandPrice(Tile tile) { Player nationOwner = tile.getOwner(); int price = 0; if (nationOwner == null || nationOwner == this) { return 0; // Freely available } else if (tile.getSettlement() != null) { return -1; // Not for sale } else if (nationOwner.isEuropean()) { if (tile.getOwningSettlement() != null && tile.getOwningSettlement().getOwner() == nationOwner) { return -1; // Nailed down by a European colony } else { return 0; // Claim abandoned or only by tile improvement } } // Else, native ownership for (GoodsType type : getSpecification().getGoodsTypeList()) { if (type == getSpecification().getPrimaryFoodType()) { // Only consider specific food types, not the aggregation. continue; } price += tile.potential(type, null); } price *= getSpecification().getIntegerOption("model.option.landPriceFactor").getValue(); price += 100; return (int) applyModifier(price, "model.modifier.landPaymentModifier", null, getGame().getTurn()); } /** * Returns whether this player has been attacked by privateers. * * @return <code>true</code> if this <code>Player</code> has been * attacked by privateers. */ public boolean getAttackedByPrivateers() { return attackedByPrivateers; } /** * Sets whether this player has been attacked by privateers. * * @param attacked True if the player has been attacked by privateers. */ public void setAttackedByPrivateers(boolean attacked) { attackedByPrivateers = attacked; } /** * Gets the default <code>Location</code> where the units arriving from * {@link Europe} will be put. * * @return The <code>Location</code>. * @see Unit#getEntryLocation */ public Location getEntryLocation() { return entryLocation; } /** * Sets the <code>Location</code> where the units arriving from * {@link Europe} will be put as a default. * * @param entryLocation The <code>Location</code>. * @see #getEntryLocation */ public void setEntryLocation(Location entryLocation) { this.entryLocation = entryLocation; } /** * Checks if this <code>Player</code> has explored the given * <code>Tile</code>. * * @param tile The <code>Tile</code>. * @return <i>true</i> if the <code>Tile</code> has been explored and * <i>false</i> otherwise. */ public boolean hasExplored(Tile tile) { return tile.isExplored(); } /** * Sets the given tile to be explored by this player and updates the * player's information about the tile. * * @param tile The <code>Tile</code> to set explored. * @see Tile#updatePlayerExploredTile(Player, boolean) */ public void setExplored(Tile tile) { logger.warning("Implemented by ServerPlayer"); } /** * Sets the tiles within the given <code>Unit</code>'s line of sight to * be explored by this player. * * @param unit The <code>Unit</code>. * @see #setExplored(Tile) * @see #hasExplored */ public void setExplored(Unit unit) { if (getGame() == null || getGame().getMap() == null || unit == null || unit.getLocation() == null || unit.getTile() == null || isIndian()) { return; } invalidateCanSeeTiles(); } /** * Forces an update of the <code>canSeeTiles</code>. This method should * be used to invalidate the current <code>canSeeTiles</code>. The method * {@link #resetCanSeeTiles} will be called whenever it is needed. */ public void invalidateCanSeeTiles() { synchronized (canSeeLock) { canSeeTiles = null; } } /** * Checks if this <code>Player</code> can see the given <code>Tile</code>. * The <code>Tile</code> can be seen if it is in a {@link Unit}'s line of * sight. * * @param tile The given <code>Tile</code>. * @return <i>true</i> if the <code>Player</code> can see the given * <code>Tile</code> and <i>false</i> otherwise. */ public boolean canSee(Tile tile) { if (tile == null) return false; do { synchronized (canSeeLock) { if (canSeeTiles != null) { return canSeeTiles[tile.getX()][tile.getY()]; } } } while (resetCanSeeTiles()); return false; } /** * Resets this player's "can see"-tiles. This is done by setting * all the tiles within each {@link Unit} and {@link Settlement}s * line of sight visible. The other tiles are made invisible. * * Use {@link #invalidateCanSeeTiles} whenever possible. * @return <code>true</code> if successful <code>false</code> otherwise */ private boolean resetCanSeeTiles() { Map map = getGame().getMap(); if (map == null) return false; boolean[][] cST = makeCanSeeTiles(map); synchronized (canSeeLock) { canSeeTiles = cST; } return true; } /** * Builds a canSeeTiles array. * * Note that tiles must be tested for null as they may be both * valid tiles but yet null during a save game load. * * Note the use of copies of the unit and settlement lists to * avoid nasty surprises due to asynchronous disappearance of * members of either. TODO: see if this can be relaxed. * * @param map The <code>Map</code> to use. * @return A canSeeTiles array. */ private boolean[][] makeCanSeeTiles(Map map) { boolean[][] cST = new boolean[map.getWidth()][map.getHeight()]; if (!getSpecification().getBoolean(GameOptions.FOG_OF_WAR)) { for (Tile t : getGame().getMap().getAllTiles()) { if (t != null) { cST[t.getX()][t.getY()] = hasExplored(t); } } } else { for (Unit unit : getUnits()) { // Only consider units directly on the map, not those // on a carrier or in Europe. if (!(unit.getLocation() instanceof Tile)) continue; Tile tile = (Tile) unit.getLocation(); cST[tile.getX()][tile.getY()] = true; for (Tile t : tile.getSurroundingTiles(unit.getLineOfSight())) { if (t != null) { cST[t.getX()][t.getY()] = hasExplored(t); } } } for (Settlement settlement : new ArrayList<Settlement>(getSettlements())) { Tile tile = settlement.getTile(); cST[tile.getX()][tile.getY()] = true; for (Tile t : tile.getSurroundingTiles(settlement.getLineOfSight())) { if (t != null) { cST[t.getX()][t.getY()] = hasExplored(t); } } } if (isEuropean() && getSpecification() .getBoolean("model.option.enhancedMissionaries")) { for (Player other : getGame().getPlayers()) { if (this.equals(other) || !other.isIndian()) continue; for (Settlement settlement : other.getSettlements()) { IndianSettlement is = (IndianSettlement) settlement; if (is.getMissionary(this) != null) { for (Tile t : is.getTile().getSurroundingTiles(is.getLineOfSight())) { if (t != null) { cST[t.getX()][t.getY()] = hasExplored(t); } } } } } } } return cST; } /** * Checks if this <code>Player</code> can build colonies. * * @return <code>true</code> if this player is european, not the royal * expeditionary force and not currently fighting the war of * independence. */ public boolean canBuildColonies() { return nationType.hasAbility("model.ability.foundColony"); } /** * Checks if this <code>Player</code> can get founding fathers. * * @return <code>true</code> if this player is european, not the royal * expeditionary force and not currently fighting the war of * independence. */ public boolean canHaveFoundingFathers() { return nationType.hasAbility("model.ability.electFoundingFather"); } /** * Determines whether this player has a certain Founding father. * * @param someFather a <code>FoundingFather</code> value * @return Whether this player has this Founding father * @see FoundingFather */ public boolean hasFather(FoundingFather someFather) { return allFathers.contains(someFather); } /** * Returns the number of founding fathers in this players congress. Used to * calculate number of liberty needed to recruit new fathers. * * @return The number of founding fathers in this players congress */ public int getFatherCount() { return allFathers.size(); } /** * Returns the founding fathers in this player's congress. * * @return the founding fathers in this player's congress. */ public Set<FoundingFather> getFathers() { return allFathers; } /** * Add a founding father to the congress. * * @param father The <code>FoundingFather</code> to add. */ public void addFather(FoundingFather father) { allFathers.add(father); addFeatures(father); for (Colony colony : getColonies()) { colony.invalidateCache(); } } /** * Gets the {@link FoundingFather founding father} this player is working * towards. * * @return The current FoundingFather or null if there is none * @see #setCurrentFather * @see FoundingFather */ public FoundingFather getCurrentFather() { return currentFather; } /** * Sets this players liberty bell production to work towards recruiting * <code>father</code> to its congress. * * @param someFather a <code>FoundingFather</code> value * @see FoundingFather */ public void setCurrentFather(FoundingFather someFather) { currentFather = someFather; } /** * Gets the set of offered fathers for this player. * * @return The current set of offered fathers. */ public List<FoundingFather> getOfferedFathers() { return offeredFathers; } /** * Clear the set of offered fathers. */ public void clearOfferedFathers() { offeredFathers.clear(); } /** * Sets the set of offered fathers. * * @param fathers A list of <code>FoundingFather</code>s to offer. */ public void setOfferedFathers(List<FoundingFather> fathers) { clearOfferedFathers(); offeredFathers.addAll(fathers); } /** * Gets the number of liberty points needed to recruit the next * founding father. * * @return How many more liberty points the <code>Player</code> * needs in order to recruit the next founding father. * @see #incrementLiberty */ public int getRemainingFoundingFatherCost() { return getTotalFoundingFatherCost() - getLiberty(); } /** * Returns how many liberty points in total are needed to earn the * Founding Father we are trying to recruit. The description of the * algorithm was taken from * http://t-a-w.blogspot.com/2007/05/colonization-tips.html * * @return Total number of liberty points the <code>Player</code> * needs to recruit the next founding father. * @see #incrementLiberty */ public int getTotalFoundingFatherCost() { int base = getSpecification() .getIntegerOption("model.option.foundingFatherFactor").getValue(); int count = getFatherCount(); return ((count + 1) * (count + 2) - 1) * base + count; } /** * Checks if this <code>Player</code> can move units to * <code>Europe</code>. * * @return <code>true</code> if this <code>Player</code> has an instance * of <code>Europe</code>. */ public boolean canMoveToEurope() { return getEurope() != null; } /** * Returns the europe object that this player has. * * @return The europe object that this player has or <code>null</code> if * this <code>Player</code> does not have an instance * <code>Europe</code>. */ public Europe getEurope() { return europe; } /** * Set the europe object for a player. * * @param europe The new <code>Europe</code> object. */ public void setEurope(Europe europe) { this.europe = europe; } /** * Describe <code>getEuropeName</code> method here. * * @return a <code>String</code> value */ public String getEuropeNameKey() { if (europe == null) { return null; } else { return nationID + ".europe"; } } /** * Returns the monarch object this player has. * * @return The monarch object this player has or <code>null</code> if this * <code>Player</code> does not have an instance * <code>Monarch</code>. */ public Monarch getMonarch() { return monarch; } /** * Sets the monarch object this player has. * * @param monarch The monarch object this player should have. */ public void setMonarch(Monarch monarch) { this.monarch = monarch; } /** * Get the <code>HighSeas</code> value. * * @return a <code>HighSeas</code> value */ public final HighSeas getHighSeas() { return highSeas; } /** * Initialize the highSeas. * Needs to be public until the backward compatibility code in * FreeColServer is gone. */ public void initializeHighSeas() { Game game = getGame(); highSeas = new HighSeas(game); if (europe != null) highSeas.addDestination(europe); if (game.getMap() != null ) highSeas.addDestination(game.getMap()); } /** * Returns the amount of gold that this player has. * * @return The amount of gold that this player has. May return * GOLD_NOT_ACCOUNTED for players whose gold is not accounted. */ public int getGold() { return gold; } /** * Set the amount of gold that this player has. * * @param newGold The new player gold value. */ public void setGold(int newGold) { gold = newGold; } /** * Checks if the player has enough gold to make a purchase. * Use this rather than comparing with getGold(), as this handles * players that do not account for gold. * * @param amount The purchase price to check. * @return True if the player can afford the purchase. */ public boolean checkGold(int amount) { return this.gold == GOLD_NOT_ACCOUNTED || this.gold >= amount; } /** * Modifies the amount of gold that this player has. The argument can be * both positive and negative. * * @param amount The amount of gold to be added to this player. * @return The amount of gold post-modification. */ public int modifyGold(int amount) { if (this.gold != Player.GOLD_NOT_ACCOUNTED) { if ((gold + amount) >= 0) { modifyScore((gold + amount) / 1000 - gold / 1000); gold += amount; } else { // This can happen if the server and the client get // out of sync. Perhaps it can also happen if the // client tries to adjust gold for another player, // where the balance is unknown. Just keep going and // do the best thing possible, we don't want to crash // the game here. logger.warning("Cannot add " + amount + " gold for " + this + ": would be negative!"); gold = 0; } } return gold; } /** * Gets an <code>Iterator</code> containing all the units this player * owns. * * @return The <code>Iterator</code>. * @see Unit */ public Iterator<Unit> getUnitIterator() { return units.values().iterator(); } public List<Unit> getUnits() { return new ArrayList<Unit>(units.values()); } /** * Gets the number of King's land units. * @return The number of units */ public int getNumberOfKingLandUnits() { int n = 0; for (Unit unit : getUnits()) { if (unit.hasAbility("model.ability.refUnit") && !unit.isNaval()) { n++; } } return n; } /** * Checks if this player has a single Man-of-War. * @return <code>true</code> if this player owns * a single Man-of-War. */ public boolean hasManOfWar() { Iterator<Unit> it = getUnitIterator(); while (it.hasNext()) { Unit unit = it.next(); if ("model.unit.manOWar".equals(unit.getType().getId())) { return true; } } return false; } /** * Gets the carrier units that can carry the supplied unit, if one exists. * * @param unit The <code>Unit</code> to carry. * @return A list of suitable carriers. */ public static List<Unit> getCarriersForUnit(Unit unit) { final Player player = unit.getOwner(); List<Unit> units = new ArrayList<Unit>(); for (Unit u : player.getUnits()) { if (u.couldCarry(unit)) units.add(u); } return units; } /** * Gets a new active unit. * * @return A <code>Unit</code> that can be made active. */ public Unit getNextActiveUnit() { return nextActiveUnitIterator.next(); } /** * Sets a new active unit. * * @param unit A <code>Unit</code> to make the next one to be active. * @return True if the operation succeeded. */ public boolean setNextActiveUnit(Unit unit) { return nextActiveUnitIterator.setNext(unit); } /** * Gets a new going_to unit. * * @return A <code>Unit</code> that can be made active. */ public Unit getNextGoingToUnit() { return nextGoingToUnitIterator.next(); } /** * Checks if a new active unit can be made active. * * @return <i>true</i> if this is the case and <i>false</i> otherwise. */ public boolean hasNextActiveUnit() { return nextActiveUnitIterator.hasNext(); } /** * Checks if a new active unit can be made active. * * @return <i>true</i> if this is the case and <i>false</i> otherwise. */ public boolean hasNextGoingToUnit() { return nextGoingToUnitIterator.hasNext(); } /** * Returns the name of this player. * * @return The name of this player. */ public String getName() { return name; } // TODO: remove this again public String getNameKey() { return getName(); } /** * Gets the name to display for this player. * TODO: This is a kludge that should be fixed. * * @return The name to display for this player. */ public String getDisplayName() { return (getName().startsWith("model.nation.")) ? Messages.message(getName()) : getName(); } /** * Is this player the unknown enemy? */ public boolean isUnknownEnemy() { return UNKNOWN_ENEMY.equals(name); } /** * Returns the name of this player. * * @return The name of this player. */ public String toString() { return getName() + " (" + nationID + ")"; } /** * Set the <code>Name</code> value. * * @param newName The new Name value. */ public void setName(String newName) { this.name = newName; } /** * Returns the nation type of this player. * * @return The nation type of this player. */ public NationType getNationType() { return nationType; } /** * Sets the nation type of this player. * * @param newNationType a <code>NationType</code> value */ public void setNationType(NationType newNationType) { if (nationType != null) removeFeatures(nationType); nationType = newNationType; if (newNationType != null) addFeatures(newNationType); } /** * Return this Player's nation. * * @return a <code>String</code> value */ public Nation getNation() { return getSpecification().getNation(nationID); } /** * Sets the nation for this player. * * @param newNation The new nation for this player. */ public void setNation(Nation newNation) { Nation oldNation = getNation(); nationID = newNation.getId(); getGame().getNationOptions().getNations().put(newNation, NationState.NOT_AVAILABLE); getGame().getNationOptions().getNations().put(oldNation, NationState.AVAILABLE); } /** * Return the ID of this Player's nation. * * @return a <code>String</code> value */ public String getNationID() { return nationID; } /** * Gets a nation name suitable for use in message IDs. * * @return a <code>String</code> value */ public String getNationNameKey() { return nationID.substring(nationID.lastIndexOf('.')+1) .toUpperCase(Locale.US); } /** * Returns the nation of this player as a String. * * @return The nation of this player as a String. */ public StringTemplate getNationName() { return (playerType == PlayerType.REBEL || playerType == PlayerType.INDEPENDENT) ? StringTemplate.name(independentNationName) : StringTemplate.key(nationID + ".name"); } /** * Get the <code>RulerName</code> value. * * @return a <code>String</code> value */ public final String getRulerNameKey() { return nationID + ".ruler"; } /** * Checks if this <code>Player</code> is ready to start the game. * * @return <code>true</code> if this <code>Player</code> is ready to * start the game. */ public boolean isReady() { return ready; } /** * Sets this <code>Player</code> to be ready/not ready for starting the * game. * * @param ready This indicates if the player is ready to start the game. */ public void setReady(boolean ready) { this.ready = ready; } public void incrementImmigration(int amount) { immigration = Math.max(0, immigration + amount); } /** * Sets the number of immigration this player possess. * * @see #incrementImmigration(int) */ public void reduceImmigration() { if (!canRecruitUnits()) { return; } int cost = getSpecification().getBoolean(GameOptions.SAVE_PRODUCTION_OVERFLOW) ? immigrationRequired : immigration; if (cost > immigration) { immigration = 0; } else { immigration -= cost; } } /** * Gets the number of immigration this player possess. * * @return The number. * @see #reduceImmigration */ public int getImmigration() { return (canRecruitUnits()) ? immigration : 0; } /** * Sets the number of immigration this player possess. * * @param immigration The immigration value for this player. */ public void setImmigration(int immigration) { if (canRecruitUnits()) { this.immigration = immigration; } } /** * Get the <code>TradeRoutes</code> value. * * @return a <code>List<TradeRoute></code> value */ public final List<TradeRoute> getTradeRoutes() { return tradeRoutes; } /** * Set the players trade routes. * * @param newTradeRoutes The new list of <code>TradeRoute</code>s. * */ public final void setTradeRoutes(final List<TradeRoute> newTradeRoutes) { tradeRoutes.clear(); tradeRoutes.addAll(newTradeRoutes); } /** * Checks to see whether or not a colonist can emigrate, and does so if * possible. * * @return Whether a new colonist should immigrate. */ public boolean checkEmigrate() { if (!canRecruitUnits()) { return false; } return getImmigrationRequired() <= immigration; } /** * Gets the number of immigration required to cause a new colonist to emigrate. * * @return The number of immigration required to cause a new colonist to * emigrate. */ public int getImmigrationRequired() { return (canRecruitUnits()) ? immigrationRequired : 0; } /** * Sets the number of immigration required to cause a new colonist to emigrate. * * @param immigrationRequired The number of immigration required to cause a new * colonist to emigrate. */ public void setImmigrationRequired(int immigrationRequired) { if (canRecruitUnits()) { this.immigrationRequired = immigrationRequired; } } /** * Updates the amount of immigration needed to emigrate a <code>Unit</code> * from <code>Europe</code>. */ public void updateImmigrationRequired() { if (!canRecruitUnits()) { return; } immigrationRequired += (int)applyModifier(getSpecification() .getIntegerOption("model.option.crossesIncrement").getValue(), "model.modifier.religiousUnrestBonus"); // The book I have tells me the crosses needed is: // [(colonist count in colonies + total colonist count) * 2] + 8. // So every unit counts as 2 unless they're in a colony, // wherein they count as 4. /* * int count = 8; Map map = getGame().getMap(); Iterator<Position> * tileIterator = map.getWholeMapIterator(); while * (tileIterator.hasNext()) { Tile t = map.getTile(tileIterator.next()); * if (t != null && t.getFirstUnit() != null && * t.getFirstUnit().getOwner().equals(this)) { Iterator<Unit> * unitIterator = t.getUnitIterator(); while (unitIterator.hasNext()) { * Unit u = unitIterator.next(); Iterator<Unit> childUnitIterator = * u.getUnitIterator(); while (childUnitIterator.hasNext()) { // Unit * childUnit = (Unit) childUnitIterator.next(); * childUnitIterator.next(); count += 2; } count += 2; } } if (t != null && * t.getColony() != null && t.getColony().getOwner() == this) { count += * t.getColony().getUnitCount() * 4; // Units in colonies // count * doubly. // -sjm } } Iterator<Unit> europeUnitIterator = * getEurope().getUnitIterator(); while (europeUnitIterator.hasNext()) { * europeUnitIterator.next(); count += 2; } if (nation == ENGLISH) { * count = (count * 2) / 3; } setCrossesRequired(count); */ } /** * Checks if this <code>Player</code> can recruit units by producing * immigration. * * @return <code>true</code> if units can be recruited by this * <code>Player</code>. */ public boolean canRecruitUnits() { return playerType == PlayerType.COLONIAL; } /** * Modifies the hostility against the given player. * * @param player The <code>Player</code>. * @param addToTension The amount to add to the current tension level. * @return A list of objects that may need updating due to the tension * change (such as native settlements). */ public List<FreeColGameObject> modifyTension(Player player, int addToTension) { return modifyTension(player, addToTension, null); } /** * Modifies the hostility against the given player. * * @param player The <code>Player</code>. * @param addToTension The amount to add to the current tension level. * @param origin A <code>Settlement</code> where the alarming event * occurred. * @return A list of objects that may need updating due to the tension * change (such as native settlements). */ public List<FreeColGameObject> modifyTension(Player player, int addToTension, Settlement origin) { if (player == null) { throw new IllegalStateException("Null player"); } else if (player == this) { throw new IllegalStateException("Self tension!"); } else if (origin != null && origin.getOwner() != this) { throw new IllegalStateException("Bogus origin:" + origin.getId()); } List<FreeColGameObject> objects = new ArrayList<FreeColGameObject>(); Tension.Level oldLevel = getTension(player).getLevel(); getTension(player).modify(addToTension); if (oldLevel != getTension(player).getLevel()) { objects.add(this); } // Propagate tension change as settlement alarm to all // settlements except the one that originated it (if any). for (Settlement settlement : settlements) { if (!settlement.equals(origin)) { if (settlement.propagateAlarm(player, addToTension)) { objects.add(settlement); } } } return objects; } /** * Sets the hostility against the given player. * * @param player The <code>Player</code>. * @param newTension The <code>Tension</code>. */ public void setTension(Player player, Tension newTension) { if (player == this || player == null) { return; } tension.put(player, newTension); } /** * Gets the hostility this player has against the given player. * * @param player The <code>Player</code>. * @return An object representing the tension level. */ public Tension getTension(Player player) { if (player == null) { throw new IllegalStateException("Null player."); } else { Tension newTension = tension.get(player); if (newTension == null) { newTension = new Tension(Tension.TENSION_MIN); } tension.put(player, newTension); return newTension; } } /** * Removes all tension with respect to a given player. Used when a * player leaves the game. * * @param player The <code>Player</code> to remove tension for. */ public void removeTension(Player player) { if (player != null) tension.remove(player); } /** * Get the <code>History</code> value. * * @return a <code>List<HistoryEvent></code> value */ public final List<HistoryEvent> getHistory() { return history; } /** * Calculates the value of an outpost-type colony at this tile. * An "outpost" is supposed to be a colony containing one worker, exporting * its whole production to europe. The value of such colony is the maximum * amount of money it can make in one turn, assuming sale of its secondary * goods plus farmed goods from one of the surrounding tiles. * * @return The value of a future colony located on this tile. This value is * used by the AI when deciding where to build a new colony. */ public int getOutpostValue(Tile t) { Market market = getMarket(); if (canClaimToFoundSettlement(t)) { boolean nearbyTileIsOcean = false; float advantages = 1f; int value = 0; for (Tile tile : t.getSurroundingTiles(1)) { if (tile.getColony() != null) { // can't build next to colony return 0; } else if (tile.getSettlement() != null) { // can build next to an indian settlement, but shouldn't SettlementType type = tile.getSettlement().getType(); if (type.getClaimableRadius() > 1) { // really shouldn't build next to cities advantages *= 0.25f; } else { advantages *= 0.5f; } } else { if (tile.isHighSeasConnected()) { nearbyTileIsOcean = true; } if (tile.getType()!=null) { for (AbstractGoods production : tile.getType().getProduction()) { GoodsType type = production.getType(); int potential = market.getSalePrice(type, tile.potential(type, null)); if (tile.getOwner() != null && !this.owns(tile)) { // tile is already owned by someone (and not by us!) if (tile.getOwner().isEuropean()) { continue; } else { potential /= 2; } } value = Math.max(value, potential); } } } } //add secondary goods being produced by a colony on this tile if (t.getType().getSecondaryGoods() != null) { GoodsType secondary = t.getType().getSecondaryGoods().getType(); value += market.getSalePrice(secondary,t.potential(secondary, null)); } if (nearbyTileIsOcean) { return Math.max(0, (int) (value * advantages)); } } return 0; } /** * Gets the value of building a <code>Colony</code> on the given tile. * This method adds bonuses to the colony value if the tile is close to (but * not overlapping with) another friendly colony. Penalties for enemy * units/colonies are added as well. * * @param tile The <code>Tile</code> * @return The value of building a colony on the given tile. */ public int getColonyValue(Tile tile) { //----- TODO: tune magic numbers //applied once final float MOD_HAS_RESOURCE = 0.75f; final float MOD_NO_PATH = 0.5f; final float MOD_LONG_PATH = 0.75f; final float MOD_FOOD_LOW = 0.75f; final float MOD_FOOD_VERY_LOW = 0.5f; //applied per goods final float MOD_BUILD_MATERIAL_MISSING = 0.10f; //applied per surrounding tile final float MOD_ADJ_SETTLEMENT_BIG = 0.25f; final float MOD_ADJ_SETTLEMENT = 0.5f; final float MOD_OWNED_EUROPEAN = 0.8f; final float MOD_OWNED_NATIVE = 0.9f; //applied per goods production, per surrounding tile final float MOD_HIGH_PRODUCTION = 1.2f; final float MOD_GOOD_PRODUCTION = 1.1f; //applied per occurrence (own colony only one-time), range-dependent. final float[] MOD_OWN_COLONY = {0.0f, 0.0f, 0.5f, 1.25f, 1.1f}; final float[] MOD_ENEMY_COLONY = {0.0f, 0.0f, 0.6f, 0.7f, 0.8f}; final float[] MOD_NEUTRAL_COLONY = {0.0f, 0.0f, 0.9f, 0.95f, 1.0f}; final float[] MOD_ENEMY_UNIT = {0.0f, 0.5f, 0.6f, 0.7f, 0.8f}; final int LONG_PATH_TILES = 16; final int PRIMARY_GOODS_VALUE = 30; //goods production in excess of this on a tile counts as good/high final int GOOD_PRODUCTION = 4; final int HIGH_PRODUCTION = 8; //counting "high" production as 2, "good" production as 1 //overall food production is considered low/very low if less than... final int FOOD_LOW = 4; final int FOOD_VERY_LOW = 2; //----- END MAGIC NUMBERS // Return -INFINITY if there is a settlement here or neighbouring. for (Tile t : tile.getSurroundingTiles(0, 1)) { if (t.getSettlement() != null) return -INFINITY; } //initialize tile value int value = 0; if (tile.getType().getPrimaryGoods() != null) { value += tile.potential(tile.getType().getPrimaryGoods().getType(), null) * PRIMARY_GOODS_VALUE; } //value += tile.potential(tile.secondaryGoods(), null) * tile.secondaryGoods().getInitialSellPrice(); //multiplicative modifier, to be applied to value later float advantage = 1f; //set up maps for all foods and building materials final Specification spec = getSpecification(); TypeCountMap<GoodsType> rawBuildingMaterialMap = new TypeCountMap<GoodsType>(); for (GoodsType g : spec.getRawBuildingGoodsTypeList()) { rawBuildingMaterialMap.incrementCount(g, 0); } TypeCountMap<GoodsType> foodMap = new TypeCountMap<GoodsType>(); for (GoodsType g : spec.getFoodGoodsTypeList()) { foodMap.incrementCount(g, 0); } // Penalty for building on a resource tile, because production // can not be improved much. if (tile.hasResource()) advantage *= MOD_HAS_RESOURCE; // Penalty if there is no direct connection to the high seas, or // if it is too long. int tilesToHighSeas = Integer.MAX_VALUE; for (Tile n : tile.getSurroundingTiles(1)) { int v = tile.getHighSeasCount(); if (v >= 0 && v < tilesToHighSeas) tilesToHighSeas = v; } if (tilesToHighSeas == Integer.MAX_VALUE) { advantage *= MOD_NO_PATH; } else if (tilesToHighSeas > LONG_PATH_TILES) { advantage *= MOD_LONG_PATH; } boolean supportingColony = false; for (int radius = 1; radius < 5; radius++) { for (Tile t : getGame().getMap().getCircleTiles(tile, false, radius)) { Settlement set = t.getSettlement(); //may be null! Colony col = t.getColony(); //may be null! if (radius==1) { //already checked: no colony here - if set!=null, it's indian if (set != null) { //penalize building next to native settlement SettlementType type = set.getType(); if (type.getClaimableRadius() > 1) { // really shouldn't build next to cities advantage *= MOD_ADJ_SETTLEMENT_BIG; } else { advantage *= MOD_ADJ_SETTLEMENT; } //no settlement on neighbouring tile } else { //apply penalty for owned neighbouring tiles if (t.getOwner() != null && !this.owns(t)) { if (t.getOwner().isEuropean()) { advantage *= MOD_OWNED_EUROPEAN; } else { advantage *= MOD_OWNED_NATIVE; } } //count production if (t.getType()!=null) { for (AbstractGoods production : t.getType().getProduction()) { GoodsType type = production.getType(); int potential = t.potential(type, null); value += potential * type.getInitialSellPrice(); // a few tiles with high production are better // than many tiles with low production int highProductionValue = 0; if (potential > HIGH_PRODUCTION) { advantage *= MOD_HIGH_PRODUCTION; highProductionValue = 2; } else if (potential > GOOD_PRODUCTION) { advantage *= MOD_GOOD_PRODUCTION; highProductionValue = 1; } if (type.isFoodType()) { foodMap.incrementCount(type, highProductionValue); } else if (type.isRawBuildingMaterial()) { rawBuildingMaterialMap.incrementCount(type, highProductionValue); } } } } //radius > 1 } else { if (value <= 0) { //value no longer changes, so return if still <=0 return 0; } if (col != null) { //apply modifier for own colony at this distance if (col.getOwner()==this) { if (!supportingColony) { supportingColony = true; advantage *= MOD_OWN_COLONY[radius]; } //apply modifier for other colonies at this distance } else { if (atWarWith(col.getOwner())) { advantage *= MOD_ENEMY_COLONY[radius]; } else { advantage *= MOD_NEUTRAL_COLONY[radius]; } } } } Iterator<Unit> ui = t.getUnitIterator(); while (ui.hasNext()) { Unit u = ui.next(); if (u.getOwner() != this && u.isOffensiveUnit() && u.getOwner().isEuropean() && atWarWith(u.getOwner())) { advantage *= MOD_ENEMY_UNIT[radius]; } } } } //check availability of key goods for (GoodsType type : rawBuildingMaterialMap.keySet()) { Integer amount = rawBuildingMaterialMap.getCount(type); if (amount == 0) { advantage *= MOD_BUILD_MATERIAL_MISSING; } } int foodProduction = 0; for (Integer food : foodMap.values()) { foodProduction += food; } if (foodProduction < FOOD_VERY_LOW) { advantage *= MOD_FOOD_VERY_LOW; } else if (foodProduction < FOOD_LOW) { advantage *= MOD_FOOD_LOW; } return (int) (value * advantage); } /** * Returns the stance towards a given player. <BR> * <BR> * One of: WAR, CEASE_FIRE, PEACE and ALLIANCE. * * @param player The <code>Player</code>. * @return The stance. */ public Stance getStance(Player player) { return (player == null || stance.get(player.getId()) == null) ? Stance.UNCONTACTED : stance.get(player.getId()); } /** * Sets the stance towards a given player to one of * WAR, CEASE_FIRE, PEACE and ALLIANCE. * * @param player The <code>Player</code>. * @param newStance The new <code>Stance</code>. * @return True if the stance change was valid. * @throws IllegalArgumentException if player is null or this. */ public boolean setStance(Player player, Stance newStance) { if (player == null) { throw new IllegalArgumentException("Player must not be 'null'."); } if (player == this) { throw new IllegalArgumentException("Cannot set the stance towards ourselves."); } if (newStance == null) { stance.remove(player.getId()); return true; } Stance oldStance = stance.get(player.getId()); if (newStance.equals(oldStance)) return true; boolean valid = true;; if ((newStance == Stance.CEASE_FIRE && oldStance != Stance.WAR) || newStance == Stance.UNCONTACTED) { valid = false; } stance.put(player.getId(), newStance); return valid; } /** * Is this player at war with the specified one. * * @param player The <code>Player</code> to check. * @return True if the players are at war. */ public boolean atWarWith(Player player) { return getStance(player) == Stance.WAR; } /** * Returns whether this player has met with the <code>Player</code> if the * given <code>nation</code>. * * @param player The Player. * @return <code>true</code> if this <code>Player</code> has contacted * the given nation. */ public boolean hasContacted(Player player) { return getStance(player) != Stance.UNCONTACTED; } /** * Returns whether this player has met with any Europeans at all. * * @return <code>true</code> if this <code>Player</code> has contacted * any Europeans. */ public boolean hasContactedEuropeans() { for (Player other : getGame().getLiveEuropeanPlayers()) { if (other != this && hasContacted(other)) { return true; } } return false; } /** * Returns whether this player has met with any natives at all. * * @return <code>true</code> if this <code>Player</code> has contacted * any natives. */ public boolean hasContactedIndians() { for (Player other : getGame().getPlayers()) { if (other != this && !other.isDead() && other.isIndian() && hasContacted(other)) { return true; } } return false; } /** * Set this player as having made initial contact with another player. * Always start with PEACE, which can go downhill fast. * * @param player1 a <code>Player</code> value * @param player2 a <code>Player</code> value */ public static void makeContact(Player player1, Player player2) { player1.stance.put(player2.getId(), Stance.PEACE); player2.stance.put(player1.getId(), Stance.PEACE); player1.setTension(player2, new Tension(Tension.TENSION_MIN)); player2.setTension(player1, new Tension(Tension.TENSION_MIN)); } /** * Has a player visited a native settlement? * * This is needed by the native report and settlement popup to * clarify whether the skill at the settlement is null because it * has been depleted, or because we just have not found out what * it is yet. * * @param settlement The <code>IndianSettlement</code> to query. * @return True if the player has visited the settlement. */ public boolean hasVisited(IndianSettlement settlement) { return settlement.hasContactedSettlement(this); } /** * Gets the price for a recruit in europe. * * @return The price of a single recruit in {@link Europe}. */ public int getRecruitPrice() { // return Math.max(0, (getCrossesRequired() - crosses) * 10); return getEurope().getRecruitPrice(); } /** * Gets the current amount of liberty this <code>Player</code> has. * * @return This player's number of liberty earned towards the * current Founding Father. */ public int getLiberty() { return (canHaveFoundingFathers()) ? liberty : 0; } /** * Sets the current amount of liberty this player has. * * @param liberty The new amount of liberty. */ public void setLiberty(int liberty) { this.liberty = liberty; } /** * Adds to the current amount of liberty this player has. * * @param amount The additional amount of liberty. */ public void incrementLiberty(int amount) { setLiberty(Math.max(0, getLiberty() + amount)); if (playerType == PlayerType.REBEL) { interventionBells += amount; } } /** * Returns how many total liberty will be produced if no colonies * are lost and nothing unexpected happens. * * @return Total number of liberty this <code>Player</code>'s * <code>Colony</code>s will make. * @see #incrementLiberty */ public int getLibertyProductionNextTurn() { int libertyNextTurn = 0; for (Colony colony : getColonies()) { for (GoodsType libertyGoods : getSpecification() .getLibertyGoodsTypeList()) { libertyNextTurn += colony.getTotalProductionOf(libertyGoods); } } return libertyNextTurn; } /** * Reset the player iterators ready for a new turn. */ public void resetIterators() { nextActiveUnitIterator.reset(); nextGoingToUnitIterator.reset(); } /** * Returns the arrears due for a type of goods. * * @param type a <code>GoodsType</code> value * @return The arrears due for this type of goods. */ public int getArrears(GoodsType type) { return getMarket().getArrears(type); } /** * Returns the arrears due for a type of goods. * * @param goods The goods. * @return The arrears due for this type of goods. */ public int getArrears(Goods goods) { return getArrears(goods.getType()); } /** * Returns true if type of goods can be traded in Europe. * * @param type The goods type. * @return True if there are no arrears due for this type of goods. */ public boolean canTrade(GoodsType type) { return canTrade(type, Market.Access.EUROPE); } /** * Returns true if type of goods can be traded at specified place. * * @param type The GoodsType. * @param access The way the goods are traded (Europe OR Custom) * @return <code>true</code> if type of goods can be traded. */ public boolean canTrade(GoodsType type, Market.Access access) { if (getMarket().getArrears(type) == 0) { return true; } if (access == Market.Access.CUSTOM_HOUSE) { if (getSpecification().getBoolean(GameOptions.CUSTOM_IGNORE_BOYCOTT)) { return true; } if (hasAbility("model.ability.customHouseTradesWithForeignCountries")) { for (Player otherPlayer : getGame().getLiveEuropeanPlayers()) { if (otherPlayer != this && (getStance(otherPlayer) == Stance.PEACE || getStance(otherPlayer) == Stance.ALLIANCE)) { return true; } } } } return false; } /** * Returns true if type of goods can be traded at specified place * * @param goods The goods. * @param access Place where the goods are traded (Europe OR Custom) * @return True if type of goods can be traded. */ public boolean canTrade(Goods goods, Market.Access access) { return canTrade(goods.getType(), access); } /** * Returns true if type of goods can be traded in Europe. * * @param goods The goods. * @return True if there are no arrears due for this type of goods. */ public boolean canTrade(Goods goods) { return canTrade(goods, Market.Access.EUROPE); } /** * Returns the current tax. * * @return The current tax. */ public int getTax() { return tax; } /** * Sets the current tax * * @param amount The new tax. */ public void setTax(int amount) { tax = amount; if (recalculateBellsBonus()) { for (Colony colony : getColonies()) { colony.invalidateCache(); } } } /** * Recalculate bells bonus when tax changes. * * @return True if a bells bonus was set. */ protected boolean recalculateBellsBonus() { Set<Modifier> libertyBonus = getModifierSet("model.goods.bells"); boolean ret = false; for (Ability ability : getAbilitySet("model.ability.addTaxToBells")) { FreeColObject source = ability.getSource(); if (source != null) { for (Modifier modifier : libertyBonus) { if (source.equals(modifier.getSource())) { modifier.setValue(tax); ret = true; } } } } return ret; } /** * Returns the current sales. * * @param goodsType a <code>GoodsType</code> value * @return The current sales. */ public int getSales(GoodsType goodsType) { return getMarket().getSales(goodsType); } /** * Modifies the current sales. * * @param goodsType a <code>GoodsType</code> value * @param amount The new sales. */ public void modifySales(GoodsType goodsType, int amount) { getMarket().modifySales(goodsType, amount); } /** * Has a type of goods been traded? * * @param goodsType a <code>GoodsType</code> value * @return Whether these goods have been traded. */ public boolean hasTraded(GoodsType goodsType) { return getMarket().hasBeenTraded(goodsType); } /** * Returns the most valuable goods available in one of the * player's colonies for the purposes of choosing a * threat-to-boycott. The goods must not currently be boycotted, * the player must have traded in it, and the amount to be discarded * will not exceed GoodsContainer.CARGO_SIZE. * * @return A goods object, or null. */ public Goods getMostValuableGoods() { if (!isEuropean()) return null; Goods goods = null; int highValue = 0; for (Colony colony : getColonies()) { for (Goods g : colony.getCompactGoods()) { if (getArrears(g.getType()) <= 0 && hasTraded(g.getType())) { int amount = Math.min(g.getAmount(), GoodsContainer.CARGO_SIZE); int value = market.getSalePrice(g.getType(), amount); if (value > highValue) { highValue = value; goods = g; } } } } return goods; } /** * Returns the current incomeBeforeTaxes. * * @param goodsType The GoodsType. * @return The current incomeBeforeTaxes. */ public int getIncomeBeforeTaxes(GoodsType goodsType) { return getMarket().getIncomeBeforeTaxes(goodsType); } /** * Modifies the current incomeBeforeTaxes. * * @param goodsType The GoodsType. * @param amount The new incomeBeforeTaxes. */ public void modifyIncomeBeforeTaxes(GoodsType goodsType, int amount) { getMarket().modifyIncomeBeforeTaxes(goodsType, amount); } /** * Returns the current incomeAfterTaxes. * * @param goodsType The GoodsType. * @return The current incomeAfterTaxes. */ public int getIncomeAfterTaxes(GoodsType goodsType) { return getMarket().getIncomeAfterTaxes(goodsType); } /** * Modifies the current incomeAfterTaxes. * * @param goodsType The GoodsType. * @param amount The new incomeAfterTaxes. */ public void modifyIncomeAfterTaxes(GoodsType goodsType, int amount) { getMarket().modifyIncomeAfterTaxes(goodsType, amount); } /** * Add a HistoryEvent to this player. * * @param event The <code>HistoryEvent</code> to add. */ public void addHistory(HistoryEvent event) { history.add(event); } /** * Checks if the given <code>Player</code> equals this object. * * @param o The <code>Player</code> to compare against this object. * @return <i>true</i> if the two <code>Player</code> are equal and none * of both have <code>nation == null</code> and <i>false</i> * otherwise. */ public boolean equals(Player o) { if (o == null) { return false; } else if (getId() == null || o.getId() == null) { // This only happens in the client code with the virtual "enemy // privateer" player // This special player is not properly associated to the Game and // therefore has no ID // TODO: remove this hack when the virtual "enemy privateer" player // is better implemented return false; } else { return getId().equals(o.getId()); } } /** * Gets the name index for a given key. * * @param key The key to use. */ public int getNameIndex(String key) { Integer val = nameIndex.get(key); return (val == null) ? 0 : val; } /** * Gets the name index for a given key. * * @param key The key to use. */ public void setNameIndex(String key, int value) { nameIndex.put(key, new Integer(value)); } /** * A predicate that can be applied to a unit. */ public abstract class UnitPredicate { public abstract boolean obtains(Unit unit); } /** * A predicate for determining active units. */ public class ActivePredicate extends UnitPredicate { private final Player player; public ActivePredicate(Player player) { this.player = player; } /** * Returns true if the unit is active, going nowhere, on a tile, * and thus available to be moved by the player. */ public boolean obtains(Unit unit) { return unit.couldMove(); } } /** * A predicate for determining units going somewhere. */ public class GoingToPredicate extends UnitPredicate { private final Player player; public GoingToPredicate(Player player) { this.player = player; } /** * Returns true if the unit has order to go somewhere. */ public boolean obtains(Unit unit) { return !unit.isDisposed() && unit.getOwner() == player && unit.getMovesLeft() > 0 && unit.getState() != Unit.UnitState.SKIPPED && (unit.getDestination() != null || unit.getTradeRoute() != null) && !(unit.getLocation() instanceof WorkLocation) && unit.getTile() != null; } } /** * Saves a LastSale record. * * @param sale The <code>LastSale</code> to save. */ public void saveSale(LastSale sale) { if (lastSales == null) lastSales = new HashMap<String, LastSale>(); lastSales.put(sale.getId(), sale); } /** * Gets the current sales data for a location and goods type. * * @param where The <code>Location</code> of the sale. * @param what The <code>GoodsType</code> sold. * * @return An appropriate <code>LastSaleData</code> record or null. */ public LastSale getLastSale(Location where, GoodsType what) { return (lastSales == null) ? null : lastSales.get(LastSale.makeKey(where, what)); } /** * Gets the last sale price for a location and goods type as a string. * * @param where The <code>Location</code> of the sale. * @param what The <code>GoodsType</code> sold. * @return An abbreviation for the sale price, or null if none found. */ public String getLastSaleString(Location where, GoodsType what) { LastSale data = getLastSale(where, what); return (data == null) ? null : String.valueOf(data.getPrice()); } /** * Get a <code>FreeColGameObject</code> with the specified id and * class, owned by this player. * * @param id The id. * @param returnClass The expected class of the object. * @return The game object, or null if not found. * @throws IllegalStateException on failure to validate the object * in any way. */ public <T extends FreeColGameObject> T getFreeColGameObject(String id, Class<T> returnClass) throws IllegalStateException { T t = getGame().getFreeColGameObject(id, returnClass); if (t == null) { throw new IllegalStateException("Not a " + returnClass.getName() + ": " + id); } else if (t instanceof Ownable) { if (this != ((Ownable)t).getOwner()) { throw new IllegalStateException(returnClass.getName() + " not owned by " + getId() + ": " + id); } } else { throw new IllegalStateException("Not ownable: " + id); } return t; } /** * An <code>Iterator</code> of {@link Unit}s that can be made active. */ public class UnitIterator implements Iterator<Unit> { private Player owner; private UnitPredicate predicate; private List<Unit> units = null; /** * A comparator to compare units by position, top to bottom, * left to right. */ private final Comparator<Unit> xyComparator = new Comparator<Unit>() { public int compare(Unit unit1, Unit unit2) { Tile tile1 = unit1.getTile(); Tile tile2 = unit2.getTile(); int cmp = ((tile1 == null) ? 0 : tile1.getY()) - ((tile2 == null) ? 0 : tile2.getY()); return (cmp != 0 || tile1 == null || tile2 == null) ? cmp : (tile1.getX() - tile2.getX()); } }; /** * Creates a new <code>UnitIterator</code>. * * @param owner The <code>Player</code> that needs an iterator of it's * units. * @param predicate An object for deciding whether a <code>Unit</code> * should be included in the <code>Iterator</code> or not. */ public UnitIterator(Player owner, UnitPredicate predicate) { this.owner = owner; this.predicate = predicate; reset(); } /** * Reset the internal units list, initially only with units that * satisfy the predicate. */ public void reset() { units = new ArrayList<Unit>(); for (Unit u : owner.getUnits()) { if (predicate.obtains(u)) units.add(u); } Collections.sort(units, xyComparator); } /** * Check if there is any more valid units. * If there are, it will be at the head of the internal units list. * * @return True if there are any valid units left. */ public boolean hasNext() { // Try to find a unit that still satisfies the predicate. while (!units.isEmpty()) { if (predicate.obtains(units.get(0))) { return true; // Still valid } units.remove(0); } // Nothing left, so refill the units list. If it is still // empty then there is definitely nothing left. reset(); return !units.isEmpty(); } /** * Get the next valid unit. * Always call hasNext to enforce validity. * * @return The next valid unit, or null if none. */ public Unit next() { return (hasNext()) ? units.remove(0) : null; } /** * Set the next valid unit. * * @param unit The <code>Unit</code> to put at the front of the list. * @return True if the operation succeeds. */ public boolean setNext(Unit unit) { if (predicate.obtains(unit)) { // Of course, it has to be valid... Unit first = (units.isEmpty()) ? null : units.get(0); while (!units.isEmpty()) { if (units.get(0) == unit) return true; units.remove(0); } reset(); while (!units.isEmpty() && units.get(0) != first) { if (units.get(0) == unit) return true; units.remove(0); } } return false; } /** * Removes from the underlying collection the last element returned by * the iterator (optional operation). * * @exception UnsupportedOperationException no matter what. */ public void remove() { throw new UnsupportedOperationException(); } } // Serialization /** * This method writes an XML-representation of this object to the given * stream. <br> * <br> * Only attributes visible to the given <code>Player</code> will be added * to that representation if <code>showAll</code> is set to * <code>false</code>. * * @param out The target stream. * @param player The <code>Player</code> this XML-representation should be * made for, or <code>null</code> if * <code>showAll == true</code>. * @param showAll Only attributes visible to <code>player</code> will be * added to the representation if <code>showAll</code> is set * to <i>false</i>. * @param toSavedGame If <code>true</code> then information that is only * needed when saving a game is added. * @throws XMLStreamException if there are any problems writing to the * stream. */ protected void toXMLImpl(XMLStreamWriter out, Player player, boolean showAll, boolean toSavedGame) throws XMLStreamException { // Start element: out.writeStartElement(getXMLElementTagName()); writeAttributes(out, player, showAll, toSavedGame); writeChildren(out, player, showAll, toSavedGame); out.writeEndElement(); } protected void writeAttributes(XMLStreamWriter out, Player player, boolean showAll, boolean toSavedGame) throws XMLStreamException { out.writeAttribute(ID_ATTRIBUTE, getId()); out.writeAttribute("username", name); out.writeAttribute("nationID", nationID); if (nationType != null) { out.writeAttribute("nationType", nationType.getId()); } out.writeAttribute("admin", Boolean.toString(admin)); out.writeAttribute("ready", Boolean.toString(ready)); out.writeAttribute("dead", Boolean.toString(dead)); out.writeAttribute("bankrupt", Boolean.toString(bankrupt)); out.writeAttribute("playerType", playerType.toString()); out.writeAttribute("ai", Boolean.toString(ai)); out.writeAttribute("tax", Integer.toString(tax)); // @compat 0.9.x out.writeAttribute("numberOfSettlements", Integer.toString(getNumberOfSettlements())); // end compatibility code if (showAll || toSavedGame || equals(player)) { out.writeAttribute("gold", Integer.toString(gold)); out.writeAttribute("immigration", Integer.toString(immigration)); out.writeAttribute("liberty", Integer.toString(liberty)); out.writeAttribute("interventionBells", Integer.toString(interventionBells)); if (currentFather != null) { out.writeAttribute("currentFather", currentFather.getId()); } out.writeAttribute("immigrationRequired", Integer.toString(immigrationRequired)); out.writeAttribute("attackedByPrivateers", Boolean.toString(attackedByPrivateers)); out.writeAttribute("oldSoL", Integer.toString(oldSoL)); out.writeAttribute("score", Integer.toString(score)); } else { out.writeAttribute("gold", Integer.toString(-1)); out.writeAttribute("immigration", Integer.toString(-1)); out.writeAttribute("liberty", Integer.toString(-1)); out.writeAttribute("immigrationRequired", Integer.toString(-1)); } if (newLandName != null) { out.writeAttribute("newLandName", newLandName); } if (independentNationName != null) { out.writeAttribute("independentNationName", independentNationName); } if (entryLocation != null) { out.writeAttribute("entryLocation", entryLocation.getId()); } for (RegionType regionType : RegionType.values()) { String key = regionType.getNameIndexKey(); int index = getNameIndex(key); if (index > 0) out.writeAttribute(key, Integer.toString(index)); } } protected void writeChildren(XMLStreamWriter out, Player player, boolean showAll, boolean toSavedGame) throws XMLStreamException { if (market != null) { market.toXML(out, player, showAll, toSavedGame); } if (showAll || toSavedGame || equals(player)) { for (Entry<Player, Tension> entry : tension.entrySet()) { out.writeStartElement(TENSION_TAG); out.writeAttribute("player", entry.getKey().getId()); out.writeAttribute(VALUE_TAG, String.valueOf(entry.getValue().getValue())); out.writeEndElement(); } for (Entry<String, Stance> entry : stance.entrySet()) { out.writeStartElement(STANCE_TAG); out.writeAttribute("player", entry.getKey()); out.writeAttribute(VALUE_TAG, entry.getValue().toString()); out.writeEndElement(); } for (HistoryEvent event : history) { event.toXML(out); } for (TradeRoute route : tradeRoutes) { route.toXML(out, this, false, false); } if (highSeas != null) { highSeas.toXMLImpl(out, player, showAll, toSavedGame); } out.writeStartElement(FOUNDING_FATHER_TAG); out.writeAttribute(ARRAY_SIZE, Integer.toString(allFathers.size())); int index = 0; for (FoundingFather father : allFathers) { out.writeAttribute("x" + Integer.toString(index), father.getId()); index++; } out.writeEndElement(); out.writeStartElement(OFFERED_FATHER_TAG); out.writeAttribute(ARRAY_SIZE, Integer.toString(offeredFathers.size())); index = 0; for (FoundingFather father : offeredFathers) { out.writeAttribute("x" + Integer.toString(index), father.getId()); index++; } out.writeEndElement(); if (europe != null) { europe.toXML(out, player, showAll, toSavedGame); } if (monarch != null) { monarch.toXML(out, player, showAll, toSavedGame); } if (!modelMessages.isEmpty()) { for (ModelMessage m : modelMessages) { m.toXML(out); } } if (lastSales != null) { for (LastSale sale : lastSales.values()) { sale.toXMLImpl(out); } } Turn turn = getGame().getTurn(); for (Modifier modifier : getModifiers()) { if (modifier.isTemporary() && !modifier.isOutOfDate(turn)) { modifier.toXML(out); } } } else { Tension t = getTension(player); if (t != null) { out.writeStartElement(TENSION_TAG); out.writeAttribute("player", player.getId()); out.writeAttribute(VALUE_TAG, String.valueOf(t.getValue())); out.writeEndElement(); } Stance s = getStance(player); for (Entry<String, Stance> entry : stance.entrySet()) { out.writeStartElement(STANCE_TAG); out.writeAttribute("player", player.getId()); out.writeAttribute(VALUE_TAG, s.toString()); out.writeEndElement(); } } } /** * Initialize this object from an XML-representation of this object. * * @param in The input stream with the XML. */ protected void readAttributes(XMLStreamReader in) throws XMLStreamException { super.readAttributes(in); name = in.getAttributeValue(null, "username"); nationID = in.getAttributeValue(null, "nationID"); if (!isUnknownEnemy()) { nationType = getSpecification().getNationType(in.getAttributeValue(null, "nationType")); } admin = getAttribute(in, "admin", false); gold = Integer.parseInt(in.getAttributeValue(null, "gold")); immigration = getAttribute(in, "immigration", 0); liberty = getAttribute(in, "liberty", 0); interventionBells = getAttribute(in, "interventionBells", 0); oldSoL = getAttribute(in, "oldSoL", 0); score = getAttribute(in, "score", 0); ready = getAttribute(in, "ready", false); ai = getAttribute(in, "ai", false); dead = getAttribute(in, "dead", false); bankrupt = getAttribute(in, "bankrupt", false); tax = Integer.parseInt(in.getAttributeValue(null, "tax")); playerType = Enum.valueOf(PlayerType.class, in.getAttributeValue(null, "playerType")); currentFather = getSpecification().getType(in, "currentFather", FoundingFather.class, null); immigrationRequired = getAttribute(in, "immigrationRequired", 12); newLandName = getAttribute(in, "newLandName", null); independentNationName = getAttribute(in, "independentNationName", null); attackedByPrivateers = getAttribute(in, "attackedByPrivateers", false); final String entryLocationStr = in.getAttributeValue(null, "entryLocation"); if (entryLocationStr != null) { FreeColGameObject fcgo = getGame().getFreeColGameObject(entryLocationStr); entryLocation = (fcgo instanceof Location) ? (Location)fcgo : new Tile(getGame(), entryLocationStr); } for (RegionType regionType : RegionType.values()) { String key = regionType.getNameIndexKey(); int index = getAttribute(in, key, -1); if (index > 0) setNameIndex(key, index); } if (nationType != null) addFeatures(nationType); switch (playerType) { case REBEL: case INDEPENDENT: addAbility(new Ability("model.ability.independenceDeclared")); break; default: // no special abilities for other playertypes, but silent warning about unused enum. break; } tension.clear(); stance.clear(); allFathers.clear(); offeredFathers.clear(); europe = null; monarch = null; history.clear(); tradeRoutes.clear(); modelMessages.clear(); lastSales = null; highSeas = null; } protected void readChildren(XMLStreamReader in) throws XMLStreamException { while (in.nextTag() != XMLStreamConstants.END_ELEMENT) { if (in.getLocalName().equals(TENSION_TAG)) { Player player = getGame().getFreeColGameObject(in.getAttributeValue(null, "player"), Player.class); tension.put(player, new Tension(getAttribute(in, VALUE_TAG, 0))); in.nextTag(); // close element } else if (in.getLocalName().equals(FOUNDING_FATHER_TAG)) { int length = Integer.parseInt(in.getAttributeValue(null, ARRAY_SIZE)); for (int index = 0; index < length; index++) { String fatherId = in.getAttributeValue(null, "x" + String.valueOf(index)); FoundingFather father = getSpecification().getFoundingFather(fatherId); addFather(father); } in.nextTag(); } else if (in.getLocalName().equals(OFFERED_FATHER_TAG)) { int length = Integer.parseInt(in.getAttributeValue(null, ARRAY_SIZE)); for (int index = 0; index < length; index++) { String fatherId = in.getAttributeValue(null, "x" + String.valueOf(index)); FoundingFather father = getSpecification().getFoundingFather(fatherId); offeredFathers.add(father); } in.nextTag(); } else if (in.getLocalName().equals(STANCE_TAG)) { String playerId = in.getAttributeValue(null, "player"); stance.put(playerId, Enum.valueOf(Stance.class, in.getAttributeValue(null, VALUE_TAG))); in.nextTag(); // close element } else if (in.getLocalName().equals(HighSeas.getXMLElementTagName())) { highSeas = updateFreeColGameObject(in, HighSeas.class); } else if (in.getLocalName().equals(Europe.getXMLElementTagName())) { europe = updateFreeColGameObject(in, Europe.class); } else if (in.getLocalName().equals(Monarch.getXMLElementTagName())) { monarch = updateFreeColGameObject(in, Monarch.class); } else if (in.getLocalName().equals(HistoryEvent.getXMLElementTagName())) { HistoryEvent event = new HistoryEvent(); event.readFromXML(in); getHistory().add(event); } else if (in.getLocalName().equals(TradeRoute.getXMLElementTagName())) { TradeRoute route = updateFreeColGameObject(in, TradeRoute.class); tradeRoutes.add(route); } else if (in.getLocalName().equals(Market.getXMLElementTagName())) { market = updateFreeColGameObject(in, Market.class); } else if (in.getLocalName().equals(ModelMessage.getXMLElementTagName())) { ModelMessage message = new ModelMessage(); message.readFromXML(in); addModelMessage(message); } else if (in.getLocalName().equals(LastSale.getXMLElementTagName())) { LastSale lastSale = new LastSale(); lastSale.readFromXML(in); saveSale(lastSale); } else if (Modifier.getXMLElementTagName().equals(in.getLocalName())) { addModifier(new Modifier(in, getSpecification())); } else { logger.warning("Unknown tag: " + in.getLocalName() + " loading player"); in.nextTag(); } } // sanity check: we should be on the closing tag if (!in.getLocalName().equals(Player.getXMLElementTagName())) { logger.warning("Error parsing xml: expecting closing tag </" + Player.getXMLElementTagName() + "> " + "found instead: " + in.getLocalName()); } // TODO: This should no longer happen. Remove soon (early 2012) // if further testing never triggers the following warning. if (market == null) { logger.warning("Null market for " + getName()); Thread.dumpStack(); market = new Market(getGame(), this); } // Bells bonuses depend on tax recalculateBellsBonus(); invalidateCanSeeTiles(); } /** * Partial writer for players, so that simple updates to fields such * as gold can be brief. * * @param out The target stream. * @param fields The fields to write. * @throws XMLStreamException If there are problems writing the stream. */ @Override protected void toXMLPartialImpl(XMLStreamWriter out, String[] fields) throws XMLStreamException { toXMLPartialByClass(out, getClass(), fields); } /** * Partial reader for players, so that simple updates to fields such * as gold can be brief. * * @param in The input stream with the XML. * @throws XMLStreamException If there are problems reading the stream. */ @Override public void readFromXMLPartialImpl(XMLStreamReader in) throws XMLStreamException { readFromXMLPartialByClass(in, getClass()); } /** * Gets the tag name of the root element representing this object. * * @return "player" */ public static String getXMLElementTagName() { return "player"; } }