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.List; import games.strategy.engine.data.Attachable; import games.strategy.engine.data.DefaultAttachment; import games.strategy.engine.data.GameData; import games.strategy.engine.data.GameParseException; import games.strategy.engine.data.PlayerID; import games.strategy.engine.data.annotations.GameProperty; import games.strategy.engine.data.changefactory.ChangeFactory; import games.strategy.engine.delegate.IDelegateBridge; import games.strategy.triplea.formatter.MyFormatter; /** * This class is designed to hold common code for holding "conditions". Any attachment that can hold conditions (ie: * RulesAttachments), * should extend this instead of DefaultAttachment. */ public abstract class AbstractConditionsAttachment extends DefaultAttachment implements ICondition { private static final long serialVersionUID = -9008441256118867078L; public static final String TRIGGER_CHANCE_SUCCESSFUL = "Trigger Rolling is a Success!"; public static final String TRIGGER_CHANCE_FAILURE = "Trigger Rolling is a Failure!"; protected static final String AND = "AND"; protected static final String OR = "OR"; protected static final String XOR = "XOR"; protected static final String DEFAULT_CHANCE = "1:1"; protected static final String CHANCE = "chance"; // list of conditions that this condition can protected ArrayList<RulesAttachment> m_conditions = new ArrayList<>(); // contain // m_conditionType modifies the relationship of m_conditions protected String m_conditionType = AND; // will logically negate the entire condition, including contained conditions protected boolean m_invert = false; // chance (x out of y) that this action is successful when attempted, default = 1:1 = always protected String m_chance = DEFAULT_CHANCE; // successful // if chance fails, we should increment the chance by x protected int m_chanceIncrementOnFailure = 0; // if chance succeeds, we should decrement the chance by x protected int m_chanceDecrementOnSuccess = 0; public AbstractConditionsAttachment(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. */ @Override @GameProperty(xmlProperty = true, gameProperty = true, adds = true) public void setConditions(final String conditions) throws GameParseException { final Collection<PlayerID> playerIDs = getData().getPlayerList().getPlayers(); for (final String subString : conditions.split(":")) { RulesAttachment condition = null; for (final PlayerID p : playerIDs) { condition = (RulesAttachment) p.getAttachment(subString); if (condition != null) { break; } } if (condition == null) { throw new GameParseException("Could not find rule. name:" + subString + thisErrorMsg()); } if (m_conditions == null) { m_conditions = new ArrayList<>(); } m_conditions.add(condition); } } @GameProperty(xmlProperty = true, gameProperty = true, adds = false) public void setConditions(final ArrayList<RulesAttachment> value) { m_conditions = value; } @Override public ArrayList<RulesAttachment> getConditions() { return m_conditions; } @Override public void clearConditions() { m_conditions.clear(); } @Override public void resetConditions() { m_conditions = new ArrayList<>(); } @Override @GameProperty(xmlProperty = true, gameProperty = true, adds = false) public void setInvert(final String s) { m_invert = getBool(s); } @Override public boolean getInvert() { return m_invert; } @Override public void resetInvert() { m_invert = false; } @Override @GameProperty(xmlProperty = true, gameProperty = true, adds = false) public void setConditionType(final String value) throws GameParseException { String s = value; if (s.equalsIgnoreCase("AND")) { s = AND; } else if (s.equalsIgnoreCase("OR")) { s = OR; } else if (s.equalsIgnoreCase("XOR")) { s = XOR; } else { final String[] nums = s.split("-"); if (nums.length == 1) { if (Integer.parseInt(nums[0]) < 0) { throw new GameParseException("conditionType must be equal to 'AND' or 'OR' or 'XOR' or 'y' or 'y-z' where Y " + "and Z are valid positive integers and Z is greater than Y" + thisErrorMsg()); } } else if (nums.length == 2) { if (Integer.parseInt(nums[0]) < 0 || Integer.parseInt(nums[1]) < 0 || !(Integer.parseInt(nums[0]) < Integer.parseInt(nums[1]))) { throw new GameParseException("conditionType must be equal to 'AND' or 'OR' or 'XOR' or 'y' or 'y-z' where Y " + "and Z are valid positive integers and Z is greater than Y" + thisErrorMsg()); } } else { throw new GameParseException("conditionType must be equal to 'AND' or 'OR' or 'XOR' or 'y' or 'y-z' where Y " + "and Z are valid positive integers and Z is greater than Y" + thisErrorMsg()); } } m_conditionType = s; } @Override public String getConditionType() { return m_conditionType; } @Override public void resetConditionType() { m_conditionType = AND; } /** * Accounts for Invert and conditionType. Only use if testedConditions has already been filled and this conditions has * been tested. */ @Override public boolean isSatisfied(final HashMap<ICondition, Boolean> testedConditions) { return isSatisfied(testedConditions, null); } /** * Accounts for Invert and conditionType. IDelegateBridge is not used so can be null, this is because we have already * tested all the * conditions. */ @Override public boolean isSatisfied(final HashMap<ICondition, Boolean> testedConditions, final IDelegateBridge aBridge) { if (testedConditions == null) { throw new IllegalStateException("testedCondititions cannot be null"); } if (testedConditions.containsKey(this)) { return testedConditions.get(this); } return areConditionsMet(new ArrayList<>(this.getConditions()), testedConditions, this.getConditionType()) != this.getInvert(); } /** * Anything that implements ICondition (currently RulesAttachment, TriggerAttachment, and PoliticalActionAttachment) * can use this to get all the conditions that must be checked for the object to be 'satisfied'. <br> * Since anything implementing ICondition can contain other ICondition, this must recursively search through all * conditions and contained * conditions to get the final list. */ public static HashSet<ICondition> getAllConditionsRecursive(final HashSet<ICondition> startingListOfConditions, HashSet<ICondition> allConditionsNeededSoFar) { if (allConditionsNeededSoFar == null) { allConditionsNeededSoFar = new HashSet<>(); } allConditionsNeededSoFar.addAll(startingListOfConditions); for (final ICondition condition : startingListOfConditions) { for (final ICondition subCondition : condition.getConditions()) { if (!allConditionsNeededSoFar.contains(subCondition)) { allConditionsNeededSoFar.addAll(getAllConditionsRecursive( new HashSet<>(Collections.singleton(subCondition)), allConditionsNeededSoFar)); } } } return allConditionsNeededSoFar; } /** * Takes the list of ICondition that getAllConditionsRecursive generates, and tests each of them, mapping them one by * one to their boolean * value. */ public static HashMap<ICondition, Boolean> testAllConditionsRecursive(final HashSet<ICondition> rules, HashMap<ICondition, Boolean> allConditionsTestedSoFar, final IDelegateBridge aBridge) { if (allConditionsTestedSoFar == null) { allConditionsTestedSoFar = new HashMap<>(); } for (final ICondition c : rules) { if (!allConditionsTestedSoFar.containsKey(c)) { testAllConditionsRecursive(new HashSet<>(c.getConditions()), allConditionsTestedSoFar, aBridge); allConditionsTestedSoFar.put(c, c.isSatisfied(allConditionsTestedSoFar, aBridge)); } } return allConditionsTestedSoFar; } /** * Accounts for all listed rules, according to the conditionType. * Takes the mapped conditions generated by testAllConditions and uses it to know which conditions are true and which * are false. There is * no testing of conditions done in this method. */ public static boolean areConditionsMet(final List<ICondition> rulesToTest, final HashMap<ICondition, Boolean> testedConditions, final String conditionType) { boolean met = false; if (conditionType.equals("AND")) { for (final ICondition c : rulesToTest) { met = testedConditions.get(c); if (!met) { break; } } } else if (conditionType.equals("OR")) { for (final ICondition c : rulesToTest) { met = testedConditions.get(c); if (met) { break; } } } else { final String[] nums = conditionType.split("-"); if (nums.length == 1) { final int start = Integer.parseInt(nums[0]); int count = 0; for (final ICondition c : rulesToTest) { met = testedConditions.get(c); if (met) { count++; } } met = (count == start); } else if (nums.length == 2) { final int start = Integer.parseInt(nums[0]); final int end = Integer.parseInt(nums[1]); int count = 0; for (final ICondition c : rulesToTest) { met = testedConditions.get(c); if (met) { count++; } } met = (count >= start && count <= end); } } return met; } @GameProperty(xmlProperty = true, gameProperty = true, adds = false) public void setChance(final String chance) throws GameParseException { final String[] s = chance.split(":"); try { final int i = getInt(s[0]); final int j = getInt(s[1]); if (i > j || i < 0 || j < 0 || i > 120 || j > 120) { throw new GameParseException( "chance should have a format of \"x:y\" where x is <= y and both x and y are >=0 and <=120" + thisErrorMsg()); } } catch (final IllegalArgumentException iae) { throw new GameParseException( "Invalid chance declaration: " + chance + " format: \"1:10\" for 10% chance" + thisErrorMsg()); } m_chance = chance; } /** * @return The number you need to roll to get the action to succeed format "1:10" for 10% chance. */ public String getChance() { return m_chance; } public int getChanceToHit() { return getInt(getChance().split(":")[0]); } public int getChanceDiceSides() { return getInt(getChance().split(":")[1]); } @GameProperty(xmlProperty = true, gameProperty = true, adds = false) public void setChanceIncrementOnFailure(final String value) { m_chanceIncrementOnFailure = getInt(value); } public int getChanceIncrementOnFailure() { return m_chanceIncrementOnFailure; } @GameProperty(xmlProperty = true, gameProperty = true, adds = false) public void setChanceDecrementOnSuccess(final String value) { m_chanceDecrementOnSuccess = getInt(value); } public int getChanceDecrementOnSuccess() { return m_chanceDecrementOnSuccess; } public void changeChanceDecrementOrIncrementOnSuccessOrFailure(final IDelegateBridge aBridge, final boolean success, final boolean historyChild) { if (success) { if (m_chanceDecrementOnSuccess == 0) { return; } final int oldToHit = getChanceToHit(); final int diceSides = getChanceDiceSides(); final int newToHit = Math.max(0, Math.min(diceSides, (oldToHit - m_chanceDecrementOnSuccess))); if (newToHit == oldToHit) { return; } final String newChance = newToHit + ":" + diceSides; aBridge.getHistoryWriter() .startEvent("Success changes chance for " + MyFormatter.attachmentNameToText(getName()) + " to " + newChance); aBridge.addChange(ChangeFactory.attachmentPropertyChange(this, newChance, CHANCE)); } else { if (m_chanceIncrementOnFailure == 0) { return; } final int oldToHit = getChanceToHit(); final int diceSides = getChanceDiceSides(); final int newToHit = Math.max(0, Math.min(diceSides, (oldToHit + m_chanceIncrementOnFailure))); if (newToHit == oldToHit) { return; } final String newChance = newToHit + ":" + diceSides; if (historyChild) { aBridge.getHistoryWriter().addChildToEvent( "Failure changes chance for " + MyFormatter.attachmentNameToText(getName()) + " to " + newChance); } else { aBridge.getHistoryWriter().startEvent( "Failure changes chance for " + MyFormatter.attachmentNameToText(getName()) + " to " + newChance); } aBridge.addChange(ChangeFactory.attachmentPropertyChange(this, newChance, CHANCE)); } } @Override public void validate(final GameData data) throws GameParseException {} }