package games.strategy.triplea.attachments; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Set; import games.strategy.engine.data.Attachable; import games.strategy.engine.data.GameData; import games.strategy.engine.data.GameMap; import games.strategy.engine.data.GameParseException; import games.strategy.engine.data.PlayerID; import games.strategy.engine.data.PlayerList; import games.strategy.engine.data.Territory; import games.strategy.engine.data.annotations.GameProperty; import games.strategy.engine.data.annotations.InternalDoNotExport; import games.strategy.triplea.delegate.Matches; import games.strategy.triplea.delegate.OriginalOwnerTracker; import games.strategy.util.Match; /** * The Purpose of this class is to hold shared and simple methods used by RulesAttachment. */ public abstract class AbstractRulesAttachment extends AbstractConditionsAttachment { private static final long serialVersionUID = -6977650137928964759L; @InternalDoNotExport // Do Not Export (do not include in IAttachment). Determines if we will be counting each for the // purposes of m_objectiveValue protected boolean m_countEach = false; @InternalDoNotExport // Do Not Export (do not include in IAttachment). The multiple that will be applied to m_objectiveValue // if m_countEach is true protected int m_eachMultiple = 1; @InternalDoNotExport // Do Not Export (do not include in IAttachment). Used with the next Territory conditions to // determine the number of territories needed to be valid (ex: m_alliedOwnershipTerritories) protected int m_territoryCount = -1; // A list of players that can be used with // directOwnershipTerritories, directExclusionTerritories, // directPresenceTerritories, or any of the other territory lists // only used if the attachment begins with "objectiveAttachment" protected ArrayList<PlayerID> m_players = new ArrayList<>(); protected int m_objectiveValue = 0; // only matters for objectiveValue, does not affect the condition protected int m_uses = -1; // condition for what turn it is protected HashMap<Integer, Integer> m_turns = null; // for on/off conditions protected boolean m_switch = true; // allows custom GameProperties protected String m_gameProperty = null; public AbstractRulesAttachment(final String name, final Attachable attachable, final GameData gameData) { super(name, attachable, gameData); } /** * Adds to, not sets. Anything that adds to instead of setting needs a clear function as well. */ @GameProperty(xmlProperty = true, gameProperty = true, adds = true) public void setPlayers(final String names) throws GameParseException { final PlayerList pl = getData().getPlayerList(); for (final String p : names.split(":")) { final PlayerID player = pl.getPlayerID(p); if (player == null) { throw new GameParseException("Could not find player. name:" + p + thisErrorMsg()); } m_players.add(player); } } @GameProperty(xmlProperty = true, gameProperty = true, adds = false) public void setPlayers(final ArrayList<PlayerID> value) { m_players = value; } public ArrayList<PlayerID> getPlayers() { if (m_players.isEmpty()) { return new ArrayList<>(Collections.singletonList((PlayerID) getAttachedTo())); } else { return m_players; } } public void clearPlayers() { m_players.clear(); } public void resetPlayers() { m_players = new ArrayList<>(); } @Override @GameProperty(xmlProperty = true, gameProperty = true, adds = false) public void setChance(final String chance) throws GameParseException { throw new GameParseException( "chance not allowed for use with RulesAttachments, instead use it with Triggers or PoliticalActions" + thisErrorMsg()); } @GameProperty(xmlProperty = true, gameProperty = true, adds = false) public void setObjectiveValue(final String value) { m_objectiveValue = getInt(value); } @GameProperty(xmlProperty = true, gameProperty = true, adds = false) public void setObjectiveValue(final Integer value) { m_objectiveValue = value; } public int getObjectiveValue() { return m_objectiveValue; } public void resetObjectiveValue() { m_objectiveValue = 0; } /** * Internal use only, is not set by xml or property utils. * Is used to determine the number of territories we need to satisfy a specific territory based condition check. * It is set multiple times during each check [isSatisfied], as there might be multiple types of territory checks * being done. So it is * just a temporary value. */ @InternalDoNotExport protected void setTerritoryCount(final String value) { if (value.equals("each")) { m_territoryCount = 1; m_countEach = true; } else { m_territoryCount = getInt(value); } } public int getTerritoryCount() { return m_territoryCount; } /** * Used to determine if there is a multiple on this national objective (if the user specified 'each' in the count. * For example, you may want to have the player receive 3 PUs for controlling each territory, in a list of * territories. */ public int getEachMultiple() { if (!getCountEach()) { return 1; } return m_eachMultiple; } protected boolean getCountEach() { return m_countEach; } @GameProperty(xmlProperty = true, gameProperty = true, adds = false) public void setUses(final String s) { m_uses = getInt(s); } @GameProperty(xmlProperty = true, gameProperty = true, adds = false) public void setUses(final Integer u) { m_uses = u; } /** * "uses" on RulesAttachments apply ONLY to giving money (PUs) to the player, they do NOT apply to the condition, and * therefore should not * be tested for in isSatisfied. */ public int getUses() { return m_uses; } public void resetUses() { m_uses = -1; } @GameProperty(xmlProperty = true, gameProperty = true, adds = false) public void setSwitch(final String value) { m_switch = getBool(value); } @GameProperty(xmlProperty = true, gameProperty = true, adds = false) public void setSwitch(final Boolean value) { m_switch = value; } public boolean getSwitch() { return m_switch; } public void resetSwitch() { m_switch = true; } @GameProperty(xmlProperty = true, gameProperty = true, adds = false) public void setGameProperty(final String value) { m_gameProperty = value; } public String getGameProperty() { return m_gameProperty; } public boolean getGamePropertyState(final GameData data) { if (m_gameProperty == null) { return false; } return data.getProperties().get(m_gameProperty, false); } public void resetGameProperty() { m_gameProperty = null; } @GameProperty(xmlProperty = true, gameProperty = true, adds = false) public void setRounds(final String rounds) throws GameParseException { if (rounds == null) { m_turns = null; return; } m_turns = new HashMap<>(); final String[] s = rounds.split(":"); if (s.length < 1) { throw new GameParseException("Empty turn list" + thisErrorMsg()); } for (final String subString : s) { int start; int end; try { start = getInt(subString); end = start; } catch (final Exception e) { final String[] s2 = subString.split("-"); if (s2.length != 2) { throw new GameParseException("Invalid syntax for turn range, must be 'int-int'" + thisErrorMsg()); } start = getInt(s2[0]); if (s2[1].equals("+")) { end = Integer.MAX_VALUE; } else { end = getInt(s2[1]); } } m_turns.put(start, end); } } @GameProperty(xmlProperty = true, gameProperty = true, adds = false) public void setTurns(final HashMap<Integer, Integer> value) { m_turns = value; } public HashMap<Integer, Integer> getTurns() { return m_turns; } public void resetTurns() { m_turns = null; } protected boolean checkTurns(final GameData data) { final int turn = data.getSequence().getRound(); for (final int t : m_turns.keySet()) { if (turn >= t && turn <= m_turns.get(t)) { return true; } } return false; } /** * Takes a string like "original", "originalNoWater", "enemy", "controlled", "controlledNoWater", "all", "map", and * turns it into an * actual list of territories. * Also sets territoryCount. */ protected Set<Territory> getTerritoriesBasedOnStringName(final String name, final Collection<PlayerID> players, final GameData data) { final GameMap gameMap = data.getMap(); if (name.equals("original") || name.equals("enemy")) { // get all originally owned territories final Set<Territory> originalTerrs = new HashSet<>(); for (final PlayerID player : players) { originalTerrs.addAll(OriginalOwnerTracker.getOriginallyOwned(data, player)); } setTerritoryCount(String.valueOf(originalTerrs.size())); return originalTerrs; } else if (name.equals("originalNoWater")) { // get all originally owned territories, but no water or impassables final Set<Territory> originalTerrs = new HashSet<>(); for (final PlayerID player : players) { originalTerrs.addAll(Match.getMatches(OriginalOwnerTracker.getOriginallyOwned(data, player), // TODO: does this account for occupiedTerrOf??? Matches.TerritoryIsNotImpassableToLandUnits(player, data))); } setTerritoryCount(String.valueOf(originalTerrs.size())); return originalTerrs; } else if (name.equals("controlled")) { final Set<Territory> ownedTerrs = new HashSet<>(); for (final PlayerID player : players) { ownedTerrs.addAll(gameMap.getTerritoriesOwnedBy(player)); } setTerritoryCount(String.valueOf(ownedTerrs.size())); return ownedTerrs; } else if (name.equals("controlledNoWater")) { final Set<Territory> ownedTerrsNoWater = new HashSet<>(); for (final PlayerID player : players) { ownedTerrsNoWater.addAll(Match.getMatches(gameMap.getTerritoriesOwnedBy(player), Matches.TerritoryIsNotImpassableToLandUnits(player, data))); } setTerritoryCount(String.valueOf(ownedTerrsNoWater.size())); return ownedTerrsNoWater; } else if (name.equals("all")) { final Set<Territory> allTerrs = new HashSet<>(); for (final PlayerID player : players) { allTerrs.addAll(gameMap.getTerritoriesOwnedBy(player)); allTerrs.addAll(OriginalOwnerTracker.getOriginallyOwned(data, player)); } setTerritoryCount(String.valueOf(allTerrs.size())); return allTerrs; } else if (name.equals("map")) { final Set<Territory> allTerrs = new HashSet<>(gameMap.getTerritories()); setTerritoryCount(String.valueOf(allTerrs.size())); return allTerrs; } else { // The list just contained 1 territory final Set<Territory> terr = new HashSet<>(); final Territory t = data.getMap().getTerritory(name); if (t == null) { throw new IllegalStateException("No territory called:" + name + thisErrorMsg()); } terr.add(t); setTerritoryCount(String.valueOf(1)); return terr; } } /** * Takes the raw data from the xml, and turns it into an actual territory list. * Will also set territoryCount. */ protected Set<Territory> getTerritoryListBasedOnInputFromXML(final String[] terrs, final Collection<PlayerID> players, final GameData data) { // If there's only 1, it might be a 'group' (original, controlled, controlledNoWater, all) if (terrs.length == 1) { return getTerritoriesBasedOnStringName(terrs[0], players, data); } else if (terrs.length == 2) { if (!terrs[1].equals("controlled") && !terrs[1].equals("controlledNoWater") && !terrs[1].equals("original") && !terrs[1].equals("originalNoWater") && !terrs[1].equals("all") && !terrs[1].equals("map") && !terrs[1].equals("enemy")) { // Get the list of territories return getListedTerritories(terrs, true, true); } else { final Set<Territory> rVal = getTerritoriesBasedOnStringName(terrs[1], players, data); // set it a second time, since getTerritoriesBasedOnStringName also sets it (so do it setTerritoryCount(String.valueOf(terrs[0])); // after the method call). return rVal; } } else { // Get the list of territories return getListedTerritories(terrs, true, true); } } protected void validateNames(final String[] terrList) { if (terrList != null && terrList.length > 0) { getListedTerritories(terrList, true, true); // removed checks for length & group commands because it breaks the setTerritoryCount feature. } } /** * Validate that all listed territories actually exist. Will return an empty list of territories if sent a list that * is empty or contains * only a "" string. */ public Set<Territory> getListedTerritories(final String[] list, final boolean testFirstItemForCount, final boolean mustSetTerritoryCount) { final Set<Territory> rVal = new HashSet<>(); // this list is null, empty, or contains "", so return a blank list of territories if (list == null || list.length == 0 || (list.length == 1 && (list[0] == null || list[0].length() == 0))) { return rVal; } boolean haveSetCount = false; for (int i = 0; i < list.length; i++) { final String name = list[i]; if (testFirstItemForCount && i == 0) { // See if the first entry contains the number of territories needed to meet the criteria try { // check if this is an integer, and if so set territory count getInt(name); if (mustSetTerritoryCount) { haveSetCount = true; setTerritoryCount(name); } continue; } catch (final Exception e) { // territory name is not an integer; fall through } } if (name.equals("each")) { m_countEach = true; if (mustSetTerritoryCount) { haveSetCount = true; setTerritoryCount(String.valueOf(1)); } continue; } // Skip looking for the territory if the original list contains one of the 'group' commands if (name.equals("controlled") || name.equals("controlledNoWater") || name.equals("original") || name.equals("originalNoWater") || name.equals("all") || name.equals("map") || name.equals("enemy")) { break; } // Validate all territories exist final Territory territory = getData().getMap().getTerritory(name); if (territory == null) { throw new IllegalStateException("No territory called:" + name + thisErrorMsg()); } rVal.add(territory); } if (mustSetTerritoryCount && !haveSetCount) { // if we have not set it, then set it to be the size of this list setTerritoryCount(String.valueOf(rVal.size())); } return rVal; } @Override public void validate(final GameData data) throws GameParseException { super.validate(data); } }