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 {}
}