package games.strategy.triplea.delegate; import java.io.Serializable; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import java.util.TreeSet; import games.strategy.engine.data.Change; import games.strategy.engine.data.CompositeChange; import games.strategy.engine.data.GameData; import games.strategy.engine.data.NamedAttachable; import games.strategy.engine.data.PlayerID; import games.strategy.engine.data.ProductionRule; import games.strategy.engine.data.RepairRule; import games.strategy.engine.data.Resource; import games.strategy.engine.data.Territory; import games.strategy.engine.data.Unit; import games.strategy.engine.data.UnitType; import games.strategy.engine.data.changefactory.ChangeFactory; import games.strategy.engine.message.IRemote; import games.strategy.triplea.Constants; import games.strategy.triplea.MapSupport; import games.strategy.triplea.TripleAUnit; import games.strategy.triplea.attachments.AbstractTriggerAttachment; import games.strategy.triplea.attachments.ICondition; import games.strategy.triplea.attachments.TechAbilityAttachment; import games.strategy.triplea.attachments.TerritoryAttachment; import games.strategy.triplea.attachments.TriggerAttachment; import games.strategy.triplea.attachments.UnitAttachment; import games.strategy.triplea.attachments.UnitTypeComparator; import games.strategy.triplea.delegate.remote.IPurchaseDelegate; import games.strategy.triplea.formatter.MyFormatter; import games.strategy.util.CompositeMatch; import games.strategy.util.CompositeMatchAnd; import games.strategy.util.CompositeMatchOr; import games.strategy.util.IntegerMap; import games.strategy.util.Match; /** * Logic for purchasing units. * Subclasses can override canAfford(...) to test if a purchase can be made * Subclasses can over ride addToPlayer(...) and removeFromPlayer(...) to change how * the adding or removing of resources is done. */ @MapSupport public class PurchaseDelegate extends BaseTripleADelegate implements IPurchaseDelegate { private boolean m_needToInitialize = true; public static final String NOT_ENOUGH_RESOURCES = "Not enough resources"; /** * Called before the delegate will run. */ @Override public void start() { super.start(); final GameData data = getData(); if (m_needToInitialize) { if (games.strategy.triplea.Properties.getTriggers(data)) { // First set up a match for what we want to have fire as a default in this delegate. List out as a composite // match OR. // use 'null, null' because this is the Default firing location for any trigger that does NOT have 'when' set. final Match<TriggerAttachment> purchaseDelegateTriggerMatch = new CompositeMatchAnd<>( AbstractTriggerAttachment.availableUses, AbstractTriggerAttachment.whenOrDefaultMatch(null, null), new CompositeMatchOr<TriggerAttachment>(TriggerAttachment.prodMatch(), TriggerAttachment.prodFrontierEditMatch(), TriggerAttachment.purchaseMatch())); // get all possible triggers based on this match. final HashSet<TriggerAttachment> toFirePossible = TriggerAttachment.collectForAllTriggersMatching( new HashSet<>(Collections.singleton(m_player)), purchaseDelegateTriggerMatch, m_bridge); if (!toFirePossible.isEmpty()) { // get all conditions possibly needed by these triggers, and then test them. final HashMap<ICondition, Boolean> testedConditions = TriggerAttachment.collectTestsForAllTriggers(toFirePossible, m_bridge); // get all triggers that are satisfied based on the tested conditions. final Set<TriggerAttachment> toFireTestedAndSatisfied = new HashSet<>( Match.getMatches(toFirePossible, AbstractTriggerAttachment.isSatisfiedMatch(testedConditions))); // now list out individual types to fire, once for each of the matches above. TriggerAttachment.triggerProductionChange(toFireTestedAndSatisfied, m_bridge, null, null, true, true, true, true); TriggerAttachment.triggerProductionFrontierEditChange(toFireTestedAndSatisfied, m_bridge, null, null, true, true, true, true); TriggerAttachment.triggerPurchase(toFireTestedAndSatisfied, m_bridge, null, null, true, true, true, true); } } giveBonusIncomeToAI(); m_needToInitialize = false; } } @Override public void end() { super.end(); m_needToInitialize = true; } @Override public Serializable saveState() { final PurchaseExtendedDelegateState state = new PurchaseExtendedDelegateState(); state.superState = super.saveState(); state.m_needToInitialize = m_needToInitialize; return state; } @Override public void loadState(final Serializable state) { final PurchaseExtendedDelegateState s = (PurchaseExtendedDelegateState) state; super.loadState(s.superState); m_needToInitialize = s.m_needToInitialize; } @Override public boolean delegateCurrentlyRequiresUserInput() { if ((m_player.getProductionFrontier() == null || m_player.getProductionFrontier().getRules().isEmpty()) && (m_player.getRepairFrontier() == null || m_player.getRepairFrontier().getRules().isEmpty())) { return false; } if (!canWePurchaseOrRepair()) { return false; } // if my capital is captured, I can't produce, but I may have PUs if I captured someone else's capital return TerritoryAttachment.doWeHaveEnoughCapitalsToProduce(m_player, getData()); } protected boolean canWePurchaseOrRepair() { if (m_player.getProductionFrontier() != null && m_player.getProductionFrontier().getRules() != null) { for (final ProductionRule rule : m_player.getProductionFrontier().getRules()) { if (m_player.getResources().has(rule.getCosts())) { return true; } } } if (m_player.getRepairFrontier() != null && m_player.getRepairFrontier().getRules() != null) { for (final RepairRule rule : m_player.getRepairFrontier().getRules()) { if (m_player.getResources().has(rule.getCosts())) { return true; } } } return false; } /** * subclasses can over ride this method to use different restrictions as to what a player can buy. */ protected boolean canAfford(final IntegerMap<Resource> costs, final PlayerID player) { return player.getResources().has(costs); } /** * Returns an error code, or null if all is good. */ @Override public String purchase(final IntegerMap<ProductionRule> productionRules) { final IntegerMap<Resource> costs = getCosts(productionRules); final IntegerMap<NamedAttachable> results = getResults(productionRules); if (!(canAfford(costs, m_player))) { return NOT_ENOUGH_RESOURCES; } // check to see if player has too many of any building with a building limit final Iterator<NamedAttachable> iter2 = results.keySet().iterator(); while (iter2.hasNext()) { final Object next = iter2.next(); if (!(next instanceof Resource)) { final UnitType type = (UnitType) next; final int quantity = results.getInt(type); final UnitAttachment ua = UnitAttachment.get(type); final int maxBuilt = ua.getMaxBuiltPerPlayer(); if (maxBuilt == 0) { return "May not build any of this unit right now: " + type.getName(); } else if (maxBuilt > 0) { // count how many units are yet to be placed or are in the field int currentlyBuilt = m_player.getUnits().countMatches(Matches.unitIsOfType(type)); final CompositeMatch<Unit> unitTypeOwnedBy = new CompositeMatchAnd<>(Matches.unitIsOfType(type), Matches.unitIsOwnedBy(m_player)); final Collection<Territory> allTerrs = getData().getMap().getTerritories(); for (final Territory t : allTerrs) { currentlyBuilt += t.getUnits().countMatches(unitTypeOwnedBy); } final int allowedBuild = maxBuilt - currentlyBuilt; if (allowedBuild - quantity < 0) { return "May only build " + allowedBuild + " of " + type.getName() + " this turn, may only build " + maxBuilt + " total"; } } } } // remove first, since add logs PUs remaining final Iterator<NamedAttachable> iter = results.keySet().iterator(); final Collection<Unit> totalUnits = new ArrayList<>(); final Collection<UnitType> totalUnitTypes = new ArrayList<>(); final Collection<Resource> totalResources = new ArrayList<>(); final Collection<NamedAttachable> totalAll = new ArrayList<>(); final CompositeChange changes = new CompositeChange(); // add changes for added resources // and find all added units while (iter.hasNext()) { final Object next = iter.next(); if (next instanceof Resource) { final Resource resource = (Resource) next; final int quantity = results.getInt(resource); final Change change = ChangeFactory.changeResourcesChange(m_player, resource, quantity); changes.add(change); for (int i = 0; i < quantity; i++) { totalResources.add(resource); } } else { final UnitType type = (UnitType) next; final int quantity = results.getInt(type); final Collection<Unit> units = type.create(quantity, m_player); totalUnits.addAll(units); for (int i = 0; i < quantity; i++) { totalUnitTypes.add(type); } } } totalAll.addAll(totalUnitTypes); totalAll.addAll(totalResources); // add changes for added units if (!totalUnits.isEmpty()) { final Change change = ChangeFactory.addUnits(m_player, totalUnits); changes.add(change); } // add changes for spent resources final String remaining = removeFromPlayer(m_player, costs, changes); // add history event String transcriptText; if (!totalUnits.isEmpty()) { transcriptText = m_player.getName() + " buy " + MyFormatter.defaultNamedToTextList(totalAll, ", ", true) + "; " + remaining; } else { transcriptText = m_player.getName() + " buy nothing; " + remaining; } m_bridge.getHistoryWriter().startEvent(transcriptText, totalUnits); // commit changes m_bridge.addChange(changes); return null; } /** * Returns an error code, or null if all is good. */ @Override public String purchaseRepair(final Map<Unit, IntegerMap<RepairRule>> repairRules) { final IntegerMap<Resource> costs = getRepairCosts(repairRules, m_player); if (!(canAfford(costs, m_player))) { return NOT_ENOUGH_RESOURCES; } if (!games.strategy.triplea.Properties.getDamageFromBombingDoneToUnitsInsteadOfTerritories(getData())) { return null; } // Get the map of the factories that were repaired and how much for each final IntegerMap<Unit> repairMap = getUnitRepairs(repairRules); if (repairMap.isEmpty()) { return null; } // remove first, since add logs PUs remaining final CompositeChange changes = new CompositeChange(); final Set<Unit> repairUnits = new HashSet<>(repairMap.keySet()); final IntegerMap<Unit> damageMap = new IntegerMap<>(); for (final Unit u : repairUnits) { final int repairCount = repairMap.getInt(u); // Display appropriate damaged/repaired factory and factory damage totals if (repairCount > 0) { final TripleAUnit taUnit = (TripleAUnit) u; final int newDamageTotal = Math.max(0, taUnit.getUnitDamage() - repairCount); if (newDamageTotal != taUnit.getUnitDamage()) { damageMap.put(u, newDamageTotal); } } } if (!damageMap.isEmpty()) { changes.add(ChangeFactory.bombingUnitDamage(damageMap)); } // add changes for spent resources final String remaining = removeFromPlayer(m_player, costs, changes); // add history event String transcriptText; if (!damageMap.isEmpty()) { transcriptText = m_player.getName() + " repair damage of " + MyFormatter.integerUnitMapToString(repairMap, ", ", "x ", true) + "; " + remaining; } else { transcriptText = m_player.getName() + " repair nothing; " + remaining; } m_bridge.getHistoryWriter().startEvent(transcriptText, new HashSet<>(damageMap.keySet())); // commit changes if (!changes.isEmpty()) { m_bridge.addChange(changes); } return null; } private IntegerMap<Unit> getUnitRepairs(final Map<Unit, IntegerMap<RepairRule>> repairRules) { final IntegerMap<Unit> repairMap = new IntegerMap<>(); for (final Unit u : repairRules.keySet()) { final IntegerMap<RepairRule> rules = repairRules.get(u); final TreeSet<RepairRule> repRules = new TreeSet<>(repairRuleComparator); repRules.addAll(rules.keySet()); for (final RepairRule repairRule : repRules) { final int quantity = rules.getInt(repairRule) * repairRule.getResults().getInt(u.getType()); repairMap.add(u, quantity); } } return repairMap; } Comparator<RepairRule> repairRuleComparator = new Comparator<RepairRule>() { UnitTypeComparator utc = new UnitTypeComparator(); @Override public int compare(final RepairRule o1, final RepairRule o2) { final UnitType u1 = (UnitType) o1.getResults().keySet().iterator().next(); final UnitType u2 = (UnitType) o2.getResults().keySet().iterator().next(); return utc.compare(u1, u2); } }; private IntegerMap<Resource> getCosts(final IntegerMap<ProductionRule> productionRules) { final IntegerMap<Resource> costs = new IntegerMap<>(); final Iterator<ProductionRule> rules = productionRules.keySet().iterator(); while (rules.hasNext()) { final ProductionRule rule = rules.next(); costs.addMultiple(rule.getCosts(), productionRules.getInt(rule)); } return costs; } private IntegerMap<Resource> getRepairCosts(final Map<Unit, IntegerMap<RepairRule>> repairRules, final PlayerID player) { final Collection<Unit> units = repairRules.keySet(); final Iterator<Unit> iter = units.iterator(); final IntegerMap<Resource> costs = new IntegerMap<>(); while (iter.hasNext()) { final Unit u = iter.next(); final Iterator<RepairRule> rules = repairRules.get(u).keySet().iterator(); while (rules.hasNext()) { final RepairRule rule = rules.next(); costs.addMultiple(rule.getCosts(), repairRules.get(u).getInt(rule)); } } final double discount = TechAbilityAttachment.getRepairDiscount(player, getData()); if (discount != 1.0D) { costs.multiplyAllValuesBy(discount, 3); } return costs; } private IntegerMap<NamedAttachable> getResults(final IntegerMap<ProductionRule> productionRules) { final IntegerMap<NamedAttachable> costs = new IntegerMap<>(); final Iterator<ProductionRule> rules = productionRules.keySet().iterator(); while (rules.hasNext()) { final ProductionRule rule = rules.next(); costs.addMultiple(rule.getResults(), productionRules.getInt(rule)); } return costs; } @Override public Class<? extends IRemote> getRemoteType() { return IPurchaseDelegate.class; } protected String removeFromPlayer(final PlayerID player, final IntegerMap<Resource> costs, final CompositeChange changes) { final StringBuffer returnString = new StringBuffer("Remaining resources: "); final IntegerMap<Resource> left = m_player.getResources().getResourcesCopy(); left.subtract(costs); for (final Entry<Resource, Integer> entry : left.entrySet()) { returnString.append(entry.getValue()).append(" ").append(entry.getKey().getName()).append("; "); } for (final Resource resource : costs.keySet()) { final float quantity = costs.getInt(resource); final int cost = (int) quantity; final Change change = ChangeFactory.changeResourcesChange(m_player, resource, -cost); changes.add(change); } return returnString.toString(); } private void giveBonusIncomeToAI() { // TODO and other resources? if (!m_player.isAI()) { return; } final int currentPUs = m_player.getResources().getQuantity(Constants.PUS); if (currentPUs <= 0) { return; } int toGive = 0; final int bonusPercent = games.strategy.triplea.Properties.getAIBonusIncomePercentage(getData()); if (bonusPercent != 0) { toGive += (int) Math.round(((double) currentPUs * (double) bonusPercent / 100)); if (toGive == 0 && bonusPercent > 0 && currentPUs > 0) { toGive += 1; } } toGive += games.strategy.triplea.Properties.getAIBonusIncomeFlatRate(getData()); if (toGive + currentPUs < 0) { toGive = currentPUs * -1; } if (toGive == 0) { return; } m_bridge.getHistoryWriter() .startEvent("Giving AI player bonus income modifier of " + toGive + MyFormatter.pluralize(" PU", toGive)); m_bridge.addChange( ChangeFactory.changeResourcesChange(m_player, getData().getResourceList().getResource(Constants.PUS), toGive)); } } class PurchaseExtendedDelegateState implements Serializable { private static final long serialVersionUID = 2326864364534284490L; Serializable superState; // add other variables here: public boolean m_needToInitialize; }