/**
* 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.List;
import java.util.Random;
import java.util.logging.Logger;
import org.freecolandroid.xml.stream.XMLStreamConstants;
import org.freecolandroid.xml.stream.XMLStreamException;
import org.freecolandroid.xml.stream.XMLStreamReader;
import org.freecolandroid.xml.stream.XMLStreamWriter;
import net.sf.freecol.common.model.Player.PlayerType;
import net.sf.freecol.common.model.Unit.Role;
import net.sf.freecol.common.option.UnitListOption;
import net.sf.freecol.common.util.RandomChoice;
import net.sf.freecol.common.util.Utils;
/**
* This class implements the player's monarch, whose functions prior
* to the revolution include raising taxes, declaring war on other
* European countries, and occasionally providing military support.
*/
public final class Monarch extends FreeColGameObject implements Named {
private static final Logger logger = Logger.getLogger(Monarch.class.getName());
/** The minimum price for mercenaries. */
public static final int MINIMUM_PRICE = 300;
/**
* The minimum tax rate (given in percentage) from where it
* can be lowered.
*/
public static final int MINIMUM_TAX_RATE = 20;
/** The name key of this monarch. */
private String name;
/** The player of this monarch. */
private Player player;
/** The number of land units in the REF. */
private final List<AbstractUnit> landUnits = new ArrayList<AbstractUnit>();
/** The number of naval units in the REF. */
private final List<AbstractUnit> navalUnits = new ArrayList<AbstractUnit>();
/** Whether a frigate has been provided. */
private boolean supportSea = false;
/** Whether displeasure has been incurred. */
private boolean displeasure = false;
// Internal variables that do not need serialization.
/** The space required to transport all land units. */
private int spaceRequired;
/** The current naval transport capacity. */
private int capacity;
/** Constants describing monarch actions. */
public static enum MonarchAction {
NO_ACTION,
RAISE_TAX_ACT,
RAISE_TAX_WAR,
FORCE_TAX,
LOWER_TAX_WAR,
LOWER_TAX_OTHER,
WAIVE_TAX,
ADD_TO_REF,
DECLARE_WAR,
SUPPORT_LAND,
SUPPORT_SEA,
OFFER_MERCENARIES, DISPLEASURE,
}
/**
* Constructor.
*
* @param game The <code>Game</code> this <code>Monarch</code>
* should be created in.
* @param player The <code>Player</code> to create the
* <code>Monarch</code> for.
* @param name The name of the <code>Monarch</code>.
*/
public Monarch(Game game, Player player, String name) {
super(game);
if (player == null) {
throw new IllegalStateException("player == null");
}
this.player = player;
this.name = name;
Specification spec = getSpecification();
List<AbstractUnit> ref =
((UnitListOption) spec.getOption("model.option.refSize")).getOptionValues();
for (AbstractUnit unit : ref) {
UnitType unitType = unit.getUnitType(spec);
if (unitType.hasAbility("model.ability.refUnit")) {
if (unitType.hasAbility(Ability.NAVAL_UNIT)) {
navalUnits.add(unit);
} else {
landUnits.add(unit);
}
} else {
logger.warning("Found REF unit lacking ability \"model.ability.refUnit\": "
+ unit.toString());
}
}
//updateSpaceAndCapacity();
}
/**
* Initiates a new <code>Monarch</code> from an <code>Element</code>
* and registers this <code>Monarch</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 Monarch(Game game, XMLStreamReader in) throws XMLStreamException {
super(game, in);
readFromXML(in);
}
/**
* Initiates a new <code>Monarch</code>
* with the given ID. The object should later be
* initialized by calling either
* {@link #readFromXML(XMLStreamReader)}.
*
* @param game The <code>Game</code> in which this object belong.
* @param id The unique identifier for this object.
*/
public Monarch(Game game, String id) {
super(game, id);
}
/**
* Update the space and capacity variables.
*/
private void updateSpaceAndCapacity() {
Specification spec = getSpecification();
capacity = 0;
for (AbstractUnit nu : navalUnits) {
if (nu.getUnitType(spec).canCarryUnits()) {
capacity += nu.getUnitType(spec).getSpace() * nu.getNumber();
}
}
spaceRequired = 0;
for (AbstractUnit lu : landUnits) {
spaceRequired += lu.getUnitType(spec).getSpaceTaken() * lu.getNumber();
}
}
/**
* Gets the sea support status.
*
* @return Gets the sea support status.
*/
public boolean getSupportSea() {
return supportSea;
}
/**
* Sets the sea support status.
*
* @param supportSea The new sea support status.
*/
public void setSupportSea(boolean supportSea) {
this.supportSea = supportSea;
}
/**
* Gets the displeasure status.
*
* @return Gets the displeasure status.
*/
public boolean getDispleasure() {
return displeasure;
}
/**
* Sets the displeasure status.
*
* @param displeasure The new displeasure status.
*/
public void setDispleasure(boolean displeasure) {
this.displeasure = displeasure;
}
/**
* Return the name key of this Monarch.
*
* @return a <code>String</code> value
*/
public String getNameKey() {
return name;
}
/**
* Gets the REF units.
*
* @return A list of all REF units.
*/
public List<AbstractUnit> getREF() {
List<AbstractUnit> result = new ArrayList<AbstractUnit>(landUnits);
result.addAll(navalUnits);
return result;
}
/**
* Gets the REF naval units.
*
* @return A list of the REF naval units.
*/
public List<AbstractUnit> getREFNavalUnits() {
return navalUnits;
}
/**
* Gets the REF land units.
*
* @return A list of the REF land units.
*/
public List<AbstractUnit> getREFLandUnits() {
return landUnits;
}
/**
* Gets the maximum tax rate in this game.
*/
private int taxMaximum() {
return getSpecification().getIntegerOption("model.option.maximumTax")
.getValue();
}
/**
* Collects a list of potential enemies for this player.
*/
public List<Player> collectPotentialEnemies() {
List<Player> enemies = new ArrayList<Player>();
// Benjamin Franklin puts an end to the monarch's interference
if (!player.hasAbility("model.ability.ignoreEuropeanWars")) {
for (Player enemy : getGame().getLiveEuropeanPlayers()) {
if (enemy.isREF()) continue;
switch (player.getStance(enemy)) {
case PEACE: case CEASE_FIRE:
enemies.add(enemy);
break;
}
}
}
return enemies;
}
/**
* Checks if a specified action is valid at present.
*
* @param action The <code>MonarchAction</code> to check.
*/
public boolean actionIsValid(MonarchAction action) {
switch (action) {
case NO_ACTION:
return true;
case RAISE_TAX_ACT:
case RAISE_TAX_WAR:
return player.getTax() < taxMaximum();
case FORCE_TAX:
return false;
case LOWER_TAX_WAR:
case LOWER_TAX_OTHER:
return player.getTax() > MINIMUM_TAX_RATE + 10;
case WAIVE_TAX:
return true;
case ADD_TO_REF:
return true;
case DECLARE_WAR:
return !collectPotentialEnemies().isEmpty();
case SUPPORT_SEA:
return player.getAttackedByPrivateers() && !getSupportSea()
&& !getDispleasure();
case SUPPORT_LAND: case OFFER_MERCENARIES:
return player.isAtWar() && !getDispleasure();
case DISPLEASURE:
return false;
default:
throw new IllegalArgumentException("Bogus monarch action: "
+ action);
}
}
/**
* Builds a weighted list of monarch actions.
*
* @return A weighted list of monarch actions.
*/
public List<RandomChoice<MonarchAction>> getActionChoices() {
List<RandomChoice<MonarchAction>> choices
= new ArrayList<RandomChoice<MonarchAction>>();
int dx = 1 + getSpecification()
.getIntegerOption("model.option.monarchMeddling").getValue();
int turn = getGame().getTurn().getNumber();
int grace = (6 - dx) * 10; // 10-50
// Nothing happens during the first few turns, if there are no
// colonies, or after the revolution begins.
if (turn < grace
|| player.getSettlements().size() == 0
|| player.getPlayerType() != PlayerType.COLONIAL) {
return choices;
}
// The more time has passed, the less likely the monarch will
// do nothing.
addIfValid(choices, MonarchAction.NO_ACTION, Math.max(200 - turn, 100));
addIfValid(choices, MonarchAction.RAISE_TAX_ACT, 5 + dx);
addIfValid(choices, MonarchAction.RAISE_TAX_WAR, 5 + dx);
addIfValid(choices, MonarchAction.LOWER_TAX_WAR, 5 - dx);
addIfValid(choices, MonarchAction.LOWER_TAX_OTHER, 5 - dx);
addIfValid(choices, MonarchAction.ADD_TO_REF, 10 + dx);
addIfValid(choices, MonarchAction.DECLARE_WAR, 5 + dx);
if (player.checkGold(MINIMUM_PRICE)) {
addIfValid(choices, MonarchAction.OFFER_MERCENARIES, 6 - dx);
} else if (dx < 3) {
addIfValid(choices, MonarchAction.SUPPORT_LAND, 3 - dx);
}
addIfValid(choices, MonarchAction.SUPPORT_SEA, 6 - dx);
return choices;
}
private void addIfValid(List<RandomChoice<MonarchAction>> choices,
MonarchAction action, int weight) {
if (actionIsValid(action)) {
choices.add(new RandomChoice<MonarchAction>(action, weight));
}
}
/**
* Calculates a tax raise.
*
* @param random The <code>Random</code> number source to use.
* @return The new tax rate.
*/
public int raiseTax(Random random) {
int taxAdjustment = getSpecification()
.getIntegerOption("model.option.taxAdjustment").getValue();
int turn = getGame().getTurn().getNumber();
int oldTax = player.getTax();
int adjust = Math.max(1, (6 - taxAdjustment) * 10); // 20-60
adjust = 1 + Utils.randomInt(logger, "Tax rise", random,
5 + turn/adjust);
return Math.min(oldTax + adjust, taxMaximum());
}
/**
* Calculates a tax reduction.
*
* @param random The <code>Random</code> number source to use.
* @return The new tax rate.
*/
public int lowerTax(Random random) {
int taxAdjustment = getSpecification()
.getIntegerOption("model.option.taxAdjustment").getValue();
int oldTax = player.getTax();
int adjust = Math.max(1, 10 - taxAdjustment); // 5-10
adjust = 1 + Utils.randomInt(logger, "Tax reduction", random, adjust);
return Math.max(oldTax - adjust, Monarch.MINIMUM_TAX_RATE);
}
/**
* Returns units to be added to the Royal Expeditionary Force.
*
* @param random The <code>Random</code> number source to use.
* @return An addition to the Royal Expeditionary Force.
*/
public AbstractUnit chooseForREF(Random random) {
AbstractUnit result = null;
// Preserve some extra naval capacity so that not all the REF
// navy is completely loaded
logger.info("Add to REF: capacity=" + capacity
+ " spaceRequired=" + spaceRequired + " => "
+ ((capacity < spaceRequired + 15) ? "naval" : "land")
+ " unit");
// TODO: magic number 2.5 * Manowar-capacity = 15
if (capacity < spaceRequired + 15) {
result = Utils.getRandomMember(logger, "Choose naval",
navalUnits, random);
result.setNumber(1);
} else {
result = Utils.getRandomMember(logger, "Choose land",
landUnits, random);
result.setNumber(Utils.randomInt(logger, "Choose land#", random, 3) + 1);
}
return result;
}
/**
* Adds units to the Royal Expeditionary Force.
*
* @param units The addition to the Royal Expeditionary Force.
*/
public void addToREF(AbstractUnit units) {
Specification spec = getSpecification();
UnitType unitType = spec.getUnitType(units.getId());
int n = units.getNumber();
if (unitType.hasAbility(Ability.NAVAL_UNIT)) {
for (AbstractUnit refUnit : navalUnits) {
if (refUnit.getId().equals(units.getId())) {
refUnit.setNumber(refUnit.getNumber() + n);
if (unitType.canCarryUnits()) {
capacity += unitType.getSpace() * n;
}
break;
}
}
} else {
for (AbstractUnit refUnit : landUnits) {
if (refUnit.getId().equals(units.getId())
&& refUnit.getRole().equals(units.getRole())) {
refUnit.setNumber(refUnit.getNumber() + n);
spaceRequired += unitType.getSpaceTaken() * n;
break;
}
}
}
updateSpaceAndCapacity();
}
/**
* Gets a additions to the colonial forces.
*
* @param random The <code>Random</code> number source to use.
* @param naval If the addition should be a naval unit.
* @return An addition to the colonial forces.
*/
public List<AbstractUnit> getSupport(Random random, boolean naval) {
Specification spec = getSpecification();
List<AbstractUnit> support = new ArrayList<AbstractUnit>();
List<UnitType> navalTypes = new ArrayList<UnitType>();
List<UnitType> bombardTypes = new ArrayList<UnitType>();
List<UnitType> mountedTypes = new ArrayList<UnitType>();
for (UnitType unitType : spec.getUnitTypeList()) {
if (unitType.hasAbility("model.ability.supportUnit")) {
if (unitType.hasAbility(Ability.NAVAL_UNIT)) {
navalTypes.add(unitType);
} else if (unitType.hasAbility(Ability.BOMBARD)) {
bombardTypes.add(unitType);
} else if (unitType.hasAbility(Ability.CAN_BE_EQUIPPED)) {
mountedTypes.add(unitType);
}
}
}
if (naval) {
support.add(new AbstractUnit(Utils.getRandomMember(logger,
"Choose naval support", navalTypes, random),
Role.DEFAULT, 1));
setSupportSea(true);
return support;
}
/**
* TODO: we should use a total strength value, and randomly
* add individual units until that limit has been
* reached. E.g. given a value of 10, we might select two
* units with strength 5, or three units with strength 3 (plus
* one with strength 1, if there is one).
*/
int difficulty = spec.getInteger("model.option.monarchSupport");
switch (difficulty) {
case 4:
support.add(new AbstractUnit(Utils.getRandomMember(logger,
"Choose bombard", bombardTypes, random),
Role.DEFAULT, 1));
support.add(new AbstractUnit(Utils.getRandomMember(logger,
"Choose mounted", mountedTypes, random),
Role.DRAGOON, 2));
break;
case 3:
support.add(new AbstractUnit(Utils.getRandomMember(logger,
"Choose mounted", mountedTypes, random),
Role.DRAGOON, 2));
support.add(new AbstractUnit(Utils.getRandomMember(logger,
"Choose soldier", mountedTypes, random),
Role.SOLDIER, 1));
break;
case 2:
support.add(new AbstractUnit(Utils.getRandomMember(logger,
"Choose mounted", mountedTypes, random),
Role.DRAGOON, 2));
break;
case 1:
support.add(new AbstractUnit(Utils.getRandomMember(logger,
"Choose mounted", mountedTypes, random),
Role.DRAGOON, 1));
support.add(new AbstractUnit(Utils.getRandomMember(logger,
"Choose soldier", mountedTypes, random),
Role.SOLDIER, 1));
break;
case 0:
support.add(new AbstractUnit(Utils.getRandomMember(logger,
"Choose soldier",mountedTypes, random),
Role.SOLDIER, 1));
break;
default:
break;
}
return support;
}
/**
* Returns units available as mercenaries.
*
* @param random The <code>Random</code> number source to use.
* @return A troop of mercenaries.
*/
public List<AbstractUnit> getMercenaries(Random random) {
Specification spec = getSpecification();
List<UnitType> unitTypes = new ArrayList<UnitType>();
for (UnitType unitType : spec.getUnitTypeList()) {
if (unitType.hasAbility("model.ability.mercenaryUnit")) {
unitTypes.add(unitType);
}
}
int mercPrice = spec.getIntegerOption("model.option.mercenaryPrice")
.getValue();
List<AbstractUnit> mercs = new ArrayList<AbstractUnit>();
int price = 0;
int limit = unitTypes.size();
UnitType unitType = null;
AbstractUnit au;
for (int count = 0; count < limit; count++) {
unitType = Utils.getRandomMember(logger, "Choose unit", unitTypes,
random);
if (unitType.hasAbility(Ability.CAN_BE_EQUIPPED)) {
for (int number = 3; number > 0; number--) {
au = new AbstractUnit(unitType, Role.DRAGOON, number);
int newPrice = player.getPrice(au) * mercPrice / 100;
if (player.checkGold(price + newPrice)) {
mercs.add(au);
price += newPrice;
break;
}
}
for (int number = 3; number > 0; number--) {
au = new AbstractUnit(unitType, Role.SOLDIER, number);
int newPrice = player.getPrice(au) * mercPrice / 100;
if (player.checkGold(price + newPrice)) {
mercs.add(au);
price += newPrice;
break;
}
}
} else {
for (int number = 3; number > 0; number--) {
au = new AbstractUnit(unitType, Role.DEFAULT, number);
int newPrice = player.getPrice(au) * mercPrice / 100;;
if (player.checkGold(price + newPrice)) {
mercs.add(au);
price += newPrice;
break;
}
}
}
unitTypes.remove(unitType);
}
/* Try to always return something, even if it is not affordable */
if (mercs.isEmpty() && unitType != null) {
Role role = (unitType.hasAbility(Ability.CAN_BE_EQUIPPED))
? Role.SOLDIER
: Role.DEFAULT;
mercs.add(new AbstractUnit(unitType, role, 1));
}
return mercs;
}
/**
* 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());
out.writeAttribute(ID_ATTRIBUTE, getId());
out.writeAttribute("player", this.player.getId());
out.writeAttribute("name", name);
out.writeAttribute("supportSea", String.valueOf(supportSea));
out.writeAttribute("displeasure", String.valueOf(displeasure));
out.writeStartElement("navalUnits");
for (AbstractUnit unit : navalUnits) {
unit.toXMLImpl(out);
}
out.writeEndElement();
out.writeStartElement("landUnits");
for (AbstractUnit unit : landUnits) {
unit.toXMLImpl(out);
}
out.writeEndElement();
out.writeEndElement();
}
/**
* Initialize this object from an XML-representation of this object.
*
* @param in The input stream with the XML.
*/
protected void readFromXMLImpl(XMLStreamReader in) throws XMLStreamException {
setId(in.getAttributeValue(null, ID_ATTRIBUTE));
player = (Player) getGame().getFreeColGameObject(in.getAttributeValue(null, "player"));
if (player == null) {
player = new Player(getGame(), in.getAttributeValue(null, "player"));
}
name = getAttribute(in, "name", player.getNation().getRulerNameKey());
supportSea = getAttribute(in, "supportSea", false);
displeasure = getAttribute(in, "displeasure", false);
navalUnits.clear();
landUnits.clear();
while (in.nextTag() != XMLStreamConstants.END_ELEMENT) {
String childName = in.getLocalName();
if ("navalUnits".equals(childName)) {
while (in.nextTag() != XMLStreamConstants.END_ELEMENT) {
AbstractUnit newUnit = new AbstractUnit(in);
navalUnits.add(newUnit);
}
} else if ("landUnits".equals(childName)) {
while (in.nextTag() != XMLStreamConstants.END_ELEMENT) {
AbstractUnit newUnit = new AbstractUnit(in);
landUnits.add(newUnit);
}
}
}
updateSpaceAndCapacity();
// sanity check: we should be on the closing tag
if (!in.getLocalName().equals(Monarch.getXMLElementTagName())) {
logger.warning("Error parsing xml: expecting closing tag </"
+ Monarch.getXMLElementTagName()
+ "> found instead: " + in.getLocalName());
}
}
/**
* Partial writer, so that simple updates 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, so that simple updates can be brief.
*
* @param in The input stream with the XML.
* @throws XMLStreamException If there are problems reading the stream.
*/
@Override
protected void readFromXMLPartialImpl(XMLStreamReader in)
throws XMLStreamException {
readFromXMLPartialByClass(in, getClass());
}
/**
* Gets the tag name of the root element representing this object.
*
* @return "monarch".
*/
public static String getXMLElementTagName() {
return "monarch";
}
}