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.Iterator; import java.util.List; import java.util.Map; import java.util.Map.Entry; import games.strategy.engine.data.Change; import games.strategy.engine.data.CompositeChange; import games.strategy.engine.data.GameData; import games.strategy.engine.data.PlayerID; 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.sound.SoundPath; import games.strategy.triplea.Constants; import games.strategy.triplea.Properties; import games.strategy.triplea.TripleAUnit; import games.strategy.triplea.attachments.PlayerAttachment; import games.strategy.triplea.attachments.RulesAttachment; import games.strategy.triplea.attachments.TerritoryAttachment; import games.strategy.triplea.attachments.UnitAttachment; import games.strategy.triplea.delegate.dataObjects.PlaceableUnits; import games.strategy.triplea.delegate.remote.IAbstractPlaceDelegate; import games.strategy.triplea.formatter.MyFormatter; import games.strategy.ui.SwingComponents; import games.strategy.util.CompositeMatch; import games.strategy.util.CompositeMatchAnd; import games.strategy.util.IntegerMap; import games.strategy.util.Match; import games.strategy.util.Tuple; /** * Logic for placing units. * * <p> * Known limitations. * Doesn't take into account limits on number of factories that can be produced. * Solved (by frigoref): * The situation where one has two non original factories a,b each with * production 2. If sea zone e neighbors a,b and sea zone f neighbors b. Then * producing 2 in e was making it such that you cannot produce in f. The reason * was that the production in e could be assigned to the factory in b, leaving no * capacity to produce in f. * A workaround was that if anyone ever accidently run into this situation * then they could undo the production, produce in f first, and then produce in e. * </p> */ public abstract class AbstractPlaceDelegate extends BaseTripleADelegate implements IAbstractPlaceDelegate { // maps Territory-> Collection of units protected Map<Territory, Collection<Unit>> m_produced = new HashMap<>(); // a list of CompositeChanges protected List<UndoablePlacement> m_placements = new ArrayList<>(); public void initialize(final String name) { initialize(name, name); } @Override public void start() { super.start(); } /** * Called before the delegate will stop running. */ @Override public void end() { super.end(); doAfterEnd(); } protected void doAfterEnd() { final PlayerID player = m_bridge.getPlayerID(); // clear all units not placed final Collection<Unit> units = player.getUnits().getUnits(); final GameData data = getData(); if (!Properties.getUnplacedUnitsLive(data) && !units.isEmpty()) { m_bridge.getHistoryWriter() .startEvent(MyFormatter.unitsToTextNoOwner(units) + " were produced but were not placed", units); final Change change = ChangeFactory.removeUnits(player, units); m_bridge.addChange(change); } // reset ourselves for next turn m_produced = new HashMap<>(); m_placements.clear(); if (GameStepPropertiesHelper.isRemoveAirThatCanNotLand(data)) { removeAirThatCantLand(); } } @Override public boolean delegateCurrentlyRequiresUserInput() { // nothing to place return !(m_player == null || (m_player.getUnits().size() == 0 && getPlacementsMade() == 0)); } protected void removeAirThatCantLand() { // for LHTR type games final GameData data = getData(); final AirThatCantLandUtil util = new AirThatCantLandUtil(m_bridge); util.removeAirThatCantLand(m_player, false); // if edit mode has been on, we need to clean up after all players for (final PlayerID player : data.getPlayerList()) { if (!player.equals(m_player)) { util.removeAirThatCantLand(player, false); } } } @Override public Serializable saveState() { final PlaceExtendedDelegateState state = new PlaceExtendedDelegateState(); state.superState = super.saveState(); state.m_produced = m_produced; state.m_placements = m_placements; return state; } @Override public void loadState(final Serializable state) { final PlaceExtendedDelegateState s = (PlaceExtendedDelegateState) state; super.loadState(s.superState); m_produced = s.m_produced; m_placements = s.m_placements; } /** * @param t territory of interest. * @return a COPY of the collection of units that are produced at territory t */ protected Collection<Unit> getAlreadyProduced(final Territory t) { // this list might be modified later final Collection<Unit> rVal = new ArrayList<>(); if (m_produced.containsKey(t)) { rVal.addAll(m_produced.get(t)); } return rVal; } @Override public int getPlacementsMade() { return m_placements.size(); } void setProduced(final Map<Territory, Collection<Unit>> produced) { m_produced = produced; } /** * @return The actual m_produced variable, allowing direct editing of the variable. */ protected final Map<Territory, Collection<Unit>> getProduced() { return m_produced; } @Override public List<UndoablePlacement> getMovesMade() { return m_placements; } @Override public String undoMove(final int moveIndex) { if (moveIndex < m_placements.size() && moveIndex >= 0) { final UndoablePlacement undoPlace = m_placements.get(moveIndex); undoPlace.undo(m_bridge); m_placements.remove(moveIndex); updateUndoablePlacementIndexes(); } return null; } private void updateUndoablePlacementIndexes() { for (int i = 0; i < m_placements.size(); i++) { m_placements.get(i).setIndex(i); } } @Override public PlaceableUnits getPlaceableUnits(final Collection<Unit> units, final Territory to) { final String error = canProduce(to, units, m_player); if (error != null) { return new PlaceableUnits(error); } final Collection<Unit> placeableUnits = getUnitsToBePlaced(to, units, m_player); final int maxUnits = getMaxUnitsToBePlaced(placeableUnits, to, m_player, true); return new PlaceableUnits(placeableUnits, maxUnits); } @Override public String placeUnits(final Collection<Unit> units, final Territory at, BidMode bidMode) { if (units == null || units.isEmpty()) { return null; } final String error = isValidPlacement(units, at, m_player); if (error != null) { return error; } // System.out.println("Placing " + MyFormatter.unitsToTextNoOwner(units) + " at " + at.getName() + " by " + // player.getName()); final List<Territory> producers = getAllProducers(at, m_player, units); producers.sort(getBestProducerComparator(at, units, m_player)); // System.out.println("Producers: " + producers); final IntegerMap<Territory> maxPlaceableMap = getMaxUnitsToBePlacedMap(units, at, m_player, true); // System.out.println("Max Place Map: " + maxPlaceableMap); final List<Unit> unitsLeftToPlace = new ArrayList<>(units); unitsLeftToPlace.sort(getUnitConstructionComparator()); // sort both producers and units so that the "to/at" territory comes first, and so that all constructions come first // this is because the PRODUCER for ALL CONSTRUCTIONS must be the SAME as the TERRITORY they are going into for (final Territory producer : producers) { if (unitsLeftToPlace.isEmpty()) { break; } // units may have special restrictions like RequiresUnits final List<Unit> unitsCanBePlacedByThisProducer; if (bidMode == BidMode.BID) { unitsCanBePlacedByThisProducer = new ArrayList<>(unitsLeftToPlace); } else { unitsCanBePlacedByThisProducer = (isUnitPlacementRestrictions() ? Match.getMatches(unitsLeftToPlace, unitWhichRequiresUnitsHasRequiredUnits(producer, true)) : new ArrayList<>(unitsLeftToPlace)); } unitsCanBePlacedByThisProducer.sort(getHardestToPlaceWithRequiresUnitsRestrictions(true)); int maxPlaceable = maxPlaceableMap.getInt(producer); if (maxPlaceable == 0 && bidMode == BidMode.NOT_BID) { continue; } else if (maxPlaceable == 0) { maxPlaceable = 1; } final int maxForThisProducer = getMaxUnitsToBePlacedFrom(producer, unitsCanBePlacedByThisProducer, at, m_player); // don't forget that -1 == infinite if (maxForThisProducer == -1 || maxForThisProducer >= unitsCanBePlacedByThisProducer.size()) { performPlaceFrom(producer, unitsCanBePlacedByThisProducer, at, m_player); unitsLeftToPlace.removeAll(unitsCanBePlacedByThisProducer); continue; } final int neededExtra = unitsCanBePlacedByThisProducer.size() - maxForThisProducer; if (maxPlaceable > maxForThisProducer) { freePlacementCapacity(producer, neededExtra, unitsCanBePlacedByThisProducer, at, m_player); final int newMaxForThisProducer = getMaxUnitsToBePlacedFrom(producer, unitsCanBePlacedByThisProducer, at, m_player); if (newMaxForThisProducer != maxPlaceable && neededExtra > newMaxForThisProducer) { throw new IllegalStateException("getMaxUnitsToBePlaced originally returned: " + maxPlaceable + ", \nWhich is not the same as it is returning after using freePlacementCapacity: " + newMaxForThisProducer + ", \nFor territory: " + at.getName() + ", Current Producer: " + producer.getName() + ", All Producers: " + producers + ", \nUnits Total: " + MyFormatter.unitsToTextNoOwner(units) + ", Units Left To Place By This Producer: " + MyFormatter.unitsToTextNoOwner(unitsCanBePlacedByThisProducer)); } } final Collection<Unit> placedUnits = Match.getNMatches(unitsCanBePlacedByThisProducer, maxPlaceable, Match.getAlwaysMatch()); performPlaceFrom(producer, placedUnits, at, m_player); unitsLeftToPlace.removeAll(placedUnits); } if (!unitsLeftToPlace.isEmpty()) { SwingComponents.showDialog("Too many units placed", "Placed above max placement"); } // play a sound if (Match.someMatch(units, Matches.UnitIsInfrastructure)) { m_bridge.getSoundChannelBroadcaster().playSoundForAll(SoundPath.CLIP_PLACED_INFRASTRUCTURE, m_player); } else if (Match.someMatch(units, Matches.UnitIsSea)) { m_bridge.getSoundChannelBroadcaster().playSoundForAll(SoundPath.CLIP_PLACED_SEA, m_player); } else if (Match.someMatch(units, Matches.UnitIsAir)) { m_bridge.getSoundChannelBroadcaster().playSoundForAll(SoundPath.CLIP_PLACED_AIR, m_player); } else { m_bridge.getSoundChannelBroadcaster().playSoundForAll(SoundPath.CLIP_PLACED_LAND, m_player); } return null; } /** * @param producer * territory that produces the new units. * @param placeableUnits * the new units * @param at * territory where the new units get placed */ protected void performPlaceFrom(final Territory producer, final Collection<Unit> placeableUnits, final Territory at, final PlayerID player) { final CompositeChange change = new CompositeChange(); // make sure we can place consuming units final boolean didIt = canWeConsumeUnits(placeableUnits, at, true, change); if (!didIt) { throw new IllegalStateException("Something wrong with consuming/upgrading units"); } final Collection<Unit> factoryAndInfrastructure = Match.getMatches(placeableUnits, Matches.UnitIsInfrastructure); if (!factoryAndInfrastructure.isEmpty()) { change.add(OriginalOwnerTracker.addOriginalOwnerChange(factoryAndInfrastructure, player)); } // can we move planes to land there final String movedAirTranscriptTextForHistory = moveAirOntoNewCarriers(at, producer, placeableUnits, player, change); final Change remove = ChangeFactory.removeUnits(player, placeableUnits); final Change place = ChangeFactory.addUnits(at, placeableUnits); change.add(remove); change.add(place); final UndoablePlacement current_placement = new UndoablePlacement(m_player, change, producer, at, placeableUnits); m_placements.add(current_placement); updateUndoablePlacementIndexes(); final String transcriptText = MyFormatter.unitsToTextNoOwner(placeableUnits) + " placed in " + at.getName(); m_bridge.getHistoryWriter().startEvent(transcriptText, current_placement.getDescriptionObject()); if (movedAirTranscriptTextForHistory != null) { m_bridge.getHistoryWriter().addChildToEvent(movedAirTranscriptTextForHistory); } m_bridge.addChange(change); updateProducedMap(producer, placeableUnits); } protected void updateProducedMap(final Territory producer, final Collection<Unit> additionallyProducedUnits) { final Collection<Unit> newProducedUnits = getAlreadyProduced(producer); newProducedUnits.addAll(additionallyProducedUnits); m_produced.put(producer, newProducedUnits); } protected void removeFromProducedMap(final Territory producer, final Collection<Unit> unitsToRemove) { final Collection<Unit> newProducedUnits = getAlreadyProduced(producer); newProducedUnits.removeAll(unitsToRemove); if (newProducedUnits.isEmpty()) { m_produced.remove(producer); } else { m_produced.put(producer, newProducedUnits); } } /** * frees the requested amount of capacity for the given producer by trying to hand over already made placements to * other territories. * This only works if one of the placements is done for another territory, more specific for a sea zone. * If such placements exists it will be tried to let them be done by other adjacent territories. * * @param producer * territory that needs more placement capacity * @param freeSize * amount of capacity that is requested */ protected void freePlacementCapacity(final Territory producer, final int freeSize, final Collection<Unit> unitsLeftToPlace, final Territory at, final PlayerID player) { int foundSpaceTotal = 0; // placements of the producer that could be redone by other territories final List<UndoablePlacement> redoPlacements = new ArrayList<>(); // territories the producer produced for (but not itself) and the amount of units it produced final HashMap<Territory, Integer> redoPlacementsCount = new HashMap<>(); // find map place territory -> possible free space for producer for (final UndoablePlacement placement : m_placements) { // find placement move of producer that can be taken over if (placement.getProducerTerritory().equals(producer)) { final Territory placeTerritory = placement.getPlaceTerritory(); // units with requiresUnits are too difficult to mess with logically, so do not move them around at all if (placeTerritory.isWater() && !placeTerritory.equals(producer) && (!isUnitPlacementRestrictions() || !Match.someMatch(placement.getUnits(), Matches.UnitRequiresUnitsOnCreation))) { // found placement move of producer that can be taken over // remember move and amount of placements in that territory redoPlacements.add(placement); final Integer integer = redoPlacementsCount.get(placeTerritory); if (integer == null) { redoPlacementsCount.put(placeTerritory, placement.getUnits().size()); } else { redoPlacementsCount.put(placeTerritory, integer + placement.getUnits().size()); } } } } // let other producers take over placements of producer // remember placement move and new territory if a placement has to be split up final Collection<Tuple<UndoablePlacement, Territory>> splitPlacements = new ArrayList<>(); for (final Entry<Territory, Integer> entry : redoPlacementsCount.entrySet()) { final Territory placeTerritory = entry.getKey(); final int maxProductionThatCanBeTakenOverFromThisPlacement = entry.getValue(); // find other producers that could produce for the placeTerritory final List<Territory> potentialNewProducers = getAllProducers(placeTerritory, player, unitsLeftToPlace); potentialNewProducers.remove(producer); Collections.sort(potentialNewProducers, getBestProducerComparator(placeTerritory, unitsLeftToPlace, player)); // we can just free a certain amount or still need a certain amount of space final int maxSpaceToBeFree = Math.min(maxProductionThatCanBeTakenOverFromThisPlacement, freeSize - foundSpaceTotal); // space that got free this on this placeTerritory int spaceAlreadyFree = 0; for (final Territory potentialNewProducerTerritory : potentialNewProducers) { int leftToPlace = getMaxUnitsToBePlacedFrom(potentialNewProducerTerritory, unitsPlacedInTerritorySoFar(placeTerritory), placeTerritory, player); if (leftToPlace == -1) { leftToPlace = maxProductionThatCanBeTakenOverFromThisPlacement; } // TODO: should we continue if leftToPlace is zero or less, now? // find placements of the producer the potentialNewProducerTerritory can take over for (final UndoablePlacement placement : redoPlacements) { if (!placement.getPlaceTerritory().equals(placeTerritory)) { continue; } final Collection<Unit> placedUnits = placement.getUnits(); final int placementSize = placedUnits.size(); // System.out.println("UndoPlacement: " + placement.getMoveLabel()); if (placementSize <= leftToPlace) { // potentialNewProducerTerritory can take over complete production placement.setProducerTerritory(potentialNewProducerTerritory); removeFromProducedMap(producer, placedUnits); updateProducedMap(potentialNewProducerTerritory, placedUnits); spaceAlreadyFree += placementSize; } else { // potentialNewProducerTerritory can take over ONLY parts of the production // remember placement and potentialNewProducerTerritory but try to avoid splitting a placement splitPlacements.add(Tuple.of(placement, potentialNewProducerTerritory)); } if (spaceAlreadyFree >= maxSpaceToBeFree) { break; } } if (spaceAlreadyFree >= maxSpaceToBeFree) { break; } } foundSpaceTotal += spaceAlreadyFree; if (foundSpaceTotal >= freeSize) { break; } } // we had a bug where we tried to split the same undoable placement twice (it can only be undone once!) boolean unusedSplitPlacments = false; final Collection<UndoablePlacement> usedUnoablePlacements = new ArrayList<>(); if (foundSpaceTotal < freeSize) { // we need to split some placement moves for (final Tuple<UndoablePlacement, Territory> tuple : splitPlacements) { final UndoablePlacement placement = tuple.getFirst(); if (usedUnoablePlacements.contains(placement)) { unusedSplitPlacments = true; continue; } final Territory newProducer = tuple.getSecond(); int leftToPlace = getMaxUnitsToBePlacedFrom(newProducer, unitsLeftToPlace, at, player); foundSpaceTotal += leftToPlace; // divide set of units that get placed final Collection<Unit> unitsForOldProducer = new ArrayList<>(placement.getUnits()); final Collection<Unit> unitsForNewProducer = new ArrayList<>(); for (final Unit unit : unitsForOldProducer) { if (leftToPlace == 0) { break; } unitsForNewProducer.add(unit); --leftToPlace; } unitsForOldProducer.removeAll(unitsForNewProducer); // split move, by undo and creating two new ones if (!unitsForNewProducer.isEmpty()) { // there is a chance we have 2 or more splitPlacements that are using the same placement (trying to split the // same placement). // So we must make sure that after we undo it the first time, it can never be undone again. usedUnoablePlacements.add(placement); undoMove(placement.getIndex()); performPlaceFrom(newProducer, unitsForNewProducer, placement.getPlaceTerritory(), player); performPlaceFrom(producer, unitsForOldProducer, placement.getPlaceTerritory(), player); } } } if (foundSpaceTotal < freeSize && unusedSplitPlacments) { freePlacementCapacity(producer, (freeSize - foundSpaceTotal), unitsLeftToPlace, at, player); } } // TODO Here's the spot for special air placement rules protected String moveAirOntoNewCarriers(final Territory at, final Territory producer, final Collection<Unit> units, final PlayerID player, final CompositeChange placeChange) { if (!at.isWater()) { return null; } if (!canMoveExistingFightersToNewCarriers() || AirThatCantLandUtil.isLHTRCarrierProduction(getData())) { return null; } if (Match.noneMatch(units, Matches.UnitIsCarrier)) { return null; } // do we have any spare carrier capacity int capacity = AirMovementValidator.carrierCapacity(units, at); // subtract fighters that have already been produced with this carrier // this turn. capacity -= AirMovementValidator.carrierCost(units); if (capacity <= 0) { return null; } if (!Matches.TerritoryIsLand.match(producer)) { return null; } if (!producer.getUnits().someMatch(Matches.UnitCanProduceUnits)) { return null; } final CompositeMatch<Unit> ownedFighters = new CompositeMatchAnd<>(Matches.UnitCanLandOnCarrier, Matches.unitIsOwnedBy(player)); if (!producer.getUnits().someMatch(ownedFighters)) { return null; } if (wasConquered(producer)) { return null; } if (Match.someMatch(getAlreadyProduced(producer), Matches.UnitCanProduceUnits)) { return null; } final List<Unit> fighters = producer.getUnits().getMatches(ownedFighters); while (fighters.size() > 0 && AirMovementValidator.carrierCost(fighters) > capacity) { fighters.remove(0); } if (fighters.size() == 0) { return null; } final Collection<Unit> movedFighters = getRemotePlayer().getNumberOfFightersToMoveToNewCarrier(fighters, producer); if (movedFighters == null || movedFighters.isEmpty()) { return null; } final Change change = ChangeFactory.moveUnits(producer, at, movedFighters); placeChange.add(change); final String transcriptText = MyFormatter.unitsToTextNoOwner(movedFighters) + " moved from " + producer.getName() + " to " + at.getName(); return transcriptText; } /** * Subclasses can over ride this to change the way placements are made. * * @return null if placement is valid */ protected String isValidPlacement(final Collection<Unit> units, final Territory at, final PlayerID player) { // do we hold enough units String error = playerHasEnoughUnits(units, at, player); if (error != null) { return error; } // can we produce that much error = canProduce(at, units, player); if (error != null) { return error; } // can we produce that much error = checkProduction(at, units, player); if (error != null) { return error; } // can we place it error = canUnitsBePlaced(at, units, player); if (error != null) { return error; } return null; } /** * Make sure the player has enough in hand to place the units. */ String playerHasEnoughUnits(final Collection<Unit> units, final Territory at, final PlayerID player) { // make sure the player has enough units in hand to place if (!player.getUnits().getUnits().containsAll(units)) { return "Not enough units"; } return null; } /** * Test whether or not the territory has the factory resources to support * the placement. AlreadyProduced maps territory->units already produced * this turn by that territory. */ protected String canProduce(final Territory to, final Collection<Unit> units, final PlayerID player) { final Collection<Territory> producers = getAllProducers(to, player, units, true); // the only reason it could be empty is if its water and no // territories adjacent have factories if (producers.isEmpty()) { return "No factory in or adjacent to " + to.getName(); } if (producers.size() == 1) { return canProduce(producers.iterator().next(), to, units, player); } final Collection<Territory> failingProducers = new ArrayList<>(); String error = ""; for (final Territory producer : producers) { final String errorP = canProduce(producer, to, units, player); if (errorP != null) { failingProducers.add(producer); // do not include the error for same territory, if water, because users do not want to see this error report for // 99.9% of games if (!(producer.equals(to) && producer.isWater())) { error += ", " + errorP; } } } if (producers.size() == failingProducers.size()) { return "Adjacent territories to " + to.getName() + " cannot produce, due to: \n " + error.replaceFirst(", ", ""); } return null; } protected String canProduce(final Territory producer, final Territory to, final Collection<Unit> units, final PlayerID player) { return canProduce(producer, to, units, player, false); } /** * Tests if this territory can produce units. (Does not check if it has space left to do so) * * @param producer * - Territory doing the producing. * @param to * - Territory to be placed in. * @param units * - Units to be placed. * @param player * - Player doing the placing. * @param simpleCheck * - If true you return true even if a factory is not present. Used when you do not want an infinite loop * (getAllProducers -> * canProduce -> howManyOfEachConstructionCanPlace -> getAllProducers -> etc) * @return - null if allowed to produce, otherwise an error String. */ protected String canProduce(final Territory producer, final Territory to, final Collection<Unit> units, final PlayerID player, final boolean simpleCheck) { // units can be null if we are just testing the territory itself... final Collection<Unit> testUnits = (units == null ? new ArrayList<>() : units); final boolean canProduceInConquered = isPlacementAllowedInCapturedTerritory(player); if (!producer.getOwner().equals(player)) { // sea constructions require either owning the sea zone or owning a surrounding land territory if (producer.isWater() && Match.someMatch(testUnits, new CompositeMatchAnd<>(Matches.UnitIsSea, Matches.UnitIsConstruction))) { boolean ownedNeighbor = false; for (final Territory current : getData().getMap().getNeighbors(to, Matches.TerritoryIsLand)) { if (current.getOwner().equals(player) && (canProduceInConquered || !wasConquered(current))) { ownedNeighbor = true; break; } } if (!ownedNeighbor) { return producer.getName() + " is not owned by you, and you have no owned neighbors which can produce"; } } else { return producer.getName() + " is not owned by you"; } } // make sure the territory wasnt conquered this turn if (!canProduceInConquered && wasConquered(producer)) { return producer.getName() + " was conquered this turn and cannot produce till next turn"; } if (isPlayerAllowedToPlacementAnyTerritoryOwnedLand(player) && Matches.TerritoryIsLand.match(to) && Matches.isTerritoryOwnedBy(player).match(to)) { return null; } if (isPlayerAllowedToPlacementAnySeaZoneByOwnedLand(player) && Matches.TerritoryIsWater.match(to) && Matches.isTerritoryOwnedBy(player).match(producer)) { return null; } if (simpleCheck) { return null; } // make sure some unit has fullfilled requiresUnits requirements if (isUnitPlacementRestrictions() && !testUnits.isEmpty() && !Match.someMatch(testUnits, unitWhichRequiresUnitsHasRequiredUnits(producer, true))) { return "You do not have the required units to build in " + producer.getName(); } if (to.isWater() && (!isWW2V2() && !isUnitPlacementInEnemySeas()) && to.getUnits().someMatch(Matches.enemyUnit(player, getData()))) { return "Cannot place sea units with enemy naval units"; } // make sure there is a factory if (wasOwnedUnitThatCanProduceUnitsOrIsFactoryInTerritoryAtStartOfStep(producer, player)) { return null; } // check to see if we are producing a factory or construction if (Match.someMatch(testUnits, Matches.UnitIsConstruction)) { if (howManyOfEachConstructionCanPlace(to, producer, testUnits, player).totalValues() > 0) { return null; } return "No more Constructions Allowed in " + producer.getName(); } // check we havent just put a factory there (should we be checking producer?) if (Match.someMatch(getAlreadyProduced(producer), Matches.UnitCanProduceUnits) || Match.someMatch(getAlreadyProduced(to), Matches.UnitCanProduceUnits)) { return "Factory in " + producer.getName() + " cant produce until 1 turn after it is created"; } return "No Factory in " + producer.getName(); } /** * Returns the territories that would do the producing if units are to be placed in a given territory. Returns an * empty list if no * suitable territory could be found. * * @param to * - Territory to place in. * @param player * - player that is placing. * @param unitsToPlace * - Can be null, otherwise is the units that will be produced. * @param simpleCheck * - If true you return true even if a factory is not present. Used when you do not want an infinite loop * (getAllProducers -> * canProduce -> howManyOfEachConstructionCanPlace -> getAllProducers -> etc) * @return - List of territories that can produce here. */ protected List<Territory> getAllProducers(final Territory to, final PlayerID player, final Collection<Unit> unitsToPlace, final boolean simpleCheck) { final List<Territory> producers = new ArrayList<>(); // if not water then must produce in that territory if (!to.isWater()) { if (simpleCheck || canProduce(to, to, unitsToPlace, player, simpleCheck) == null) { producers.add(to); } return producers; } if (canProduce(to, to, unitsToPlace, player, simpleCheck) == null) { producers.add(to); } for (final Territory current : getData().getMap().getNeighbors(to, Matches.TerritoryIsLand)) { if (canProduce(current, to, unitsToPlace, player, simpleCheck) == null) { producers.add(current); } } return producers; } /** * Test whether or not the territory has the factory resources to support * the placement. AlreadyProduced maps territory->units already produced * this turn by that territory. */ protected String checkProduction(final Territory to, final Collection<Unit> units, final PlayerID player) { final List<Territory> producers = getAllProducers(to, player, units); if (producers.isEmpty()) { return "No factory in or adjacent to " + to.getName(); } // if its an original factory then unlimited production Collections.sort(producers, getBestProducerComparator(to, units, player)); if (!getCanAllUnitsWithRequiresUnitsBePlacedCorrectly(units, to)) { return "Cannot place more units which require units, than production capacity of territories with the required " + "units"; } final int maxUnitsToBePlaced = getMaxUnitsToBePlaced(units, to, player, true); if ((maxUnitsToBePlaced != -1) && (maxUnitsToBePlaced < units.size())) { return "Cannot place " + units.size() + " more units in " + to.getName(); } return null; } public String canUnitsBePlaced(final Territory to, final Collection<Unit> units, final PlayerID player) { final Collection<Unit> allowedUnits = getUnitsToBePlaced(to, units, player); if (allowedUnits == null || !allowedUnits.containsAll(units)) { return "Cannot place these units in " + to.getName(); } final IntegerMap<String> constructionMap = howManyOfEachConstructionCanPlace(to, to, units, player); for (final Unit currentUnit : Match.getMatches(units, Matches.UnitIsConstruction)) { final UnitAttachment ua = UnitAttachment.get(currentUnit.getUnitType()); /* * if (ua.getIsFactory() && !ua.getIsConstruction()) * constructionMap.add("factory", -1); * else */ constructionMap.add(ua.getConstructionType(), -1); } if (!constructionMap.isPositive()) { return "Too many constructions in " + to.getName(); } final List<Territory> capitalsListOwned = new ArrayList<>(TerritoryAttachment.getAllCurrentlyOwnedCapitals(player, getData())); if (!capitalsListOwned.contains(to) && isPlacementInCapitalRestricted(player)) { return "Cannot place these units outside of the capital"; } if (to.isWater()) { final String canLand = validateNewAirCanLandOnCarriers(to, units); if (canLand != null) { return canLand; } } else { // make sure we own the territory if (!to.getOwner().equals(player)) { if (GameStepPropertiesHelper.isBid(getData())) { final PlayerAttachment pa = PlayerAttachment.get(to.getOwner()); if ((pa == null || pa.getGiveUnitControl() == null || !pa.getGiveUnitControl().contains(player)) && !to.getUnits().someMatch(Matches.unitIsOwnedBy(player))) { return "You don't own " + to.getName(); } } else { return "You don't own " + to.getName(); } } // make sure all units are land if (!Match.allMatch(units, Matches.UnitIsNotSea)) { return "Cant place sea units on land"; } } // make sure we can place consuming units if (!canWeConsumeUnits(units, to, false, null)) { return "Not Enough Units To Upgrade or Be Consumed"; } // now check for stacking limits final Collection<UnitType> typesAlreadyChecked = new ArrayList<>(); for (final Unit currentUnit : units) { final UnitType ut = currentUnit.getType(); if (typesAlreadyChecked.contains(ut)) { continue; } typesAlreadyChecked.add(ut); final int maxForThisType = UnitAttachment.getMaximumNumberOfThisUnitTypeToReachStackingLimit("placementLimit", ut, to, player, getData()); if (Match.countMatches(units, Matches.unitIsOfType(ut)) > maxForThisType) { return "UnitType " + ut.getName() + " is over stacking limit of " + maxForThisType; } } if (!PlayerAttachment.getCanTheseUnitsMoveWithoutViolatingStackingLimit("placementLimit", units, to, player, getData())) { return "Units Cannot Go Over Stacking Limit"; } // now return null (valid placement) if we have placement restrictions disabled in game options if (!isUnitPlacementRestrictions()) { return null; } // account for any unit placement restrictions by territory for (final Unit currentUnit : units) { final UnitAttachment ua = UnitAttachment.get(currentUnit.getUnitType()); // Can be null! final TerritoryAttachment ta = TerritoryAttachment.get(to); if (ua.getCanOnlyBePlacedInTerritoryValuedAtX() != -1 && ua.getCanOnlyBePlacedInTerritoryValuedAtX() > (ta == null ? 0 : ta.getProduction())) { return "Cannot place these units in " + to.getName() + " due to Unit Placement Restrictions on Territory Value"; } final String[] terrs = ua.getUnitPlacementRestrictions(); final Collection<Territory> listedTerrs = getListedTerritories(terrs); if (listedTerrs.contains(to)) { return "Cannot place these units in " + to.getName() + " due to Unit Placement Restrictions"; } if (Matches.UnitCanOnlyPlaceInOriginalTerritories.match(currentUnit) && !Matches.TerritoryIsOriginallyOwnedBy(player).match(to)) { return "Cannot place these units in " + to.getName() + " as territory is not originally owned"; } } return null; } // Separate it out so we can Override it in sub classes. protected Collection<Unit> getUnitsToBePlaced(final Territory to, final Collection<Unit> units, final PlayerID player) { if (to.isWater()) { return getUnitsToBePlacedSea(to, units, player); } // if land return getUnitsToBePlacedLand(to, units, player); } protected Collection<Unit> getUnitsToBePlacedSea(final Territory to, final Collection<Unit> units, final PlayerID player) { return getUnitsToBePlacedAllDefault(to, units, player); } protected Collection<Unit> getUnitsToBePlacedLand(final Territory to, final Collection<Unit> units, final PlayerID player) { return getUnitsToBePlacedAllDefault(to, units, player); } protected Collection<Unit> getUnitsToBePlacedAllDefault(final Territory to, final Collection<Unit> allUnits, final PlayerID player) { final boolean water = to.isWater(); if (water && (!isWW2V2() && !isUnitPlacementInEnemySeas()) && to.getUnits().someMatch(Matches.enemyUnit(player, getData()))) { return null; } final Collection<Unit> units = new ArrayList<>(allUnits); // if water, remove land. if land, remove water. units.removeAll(Match.getMatches(units, water ? Matches.UnitIsLand : Matches.UnitIsSea)); final Collection<Unit> placeableUnits = new ArrayList<>(); final Collection<Unit> unitsAtStartOfTurnInTO = unitsAtStartOfStepInTerritory(to); final Collection<Unit> allProducedUnits = unitsPlacedInTerritorySoFar(to); final boolean isBid = GameStepPropertiesHelper.isBid(getData()); final boolean wasFactoryThereAtStart = wasOwnedUnitThatCanProduceUnitsOrIsFactoryInTerritoryAtStartOfStep(to, player); // we add factories and constructions later if (water || wasFactoryThereAtStart || (!water && isPlayerAllowedToPlacementAnyTerritoryOwnedLand(player))) { final Match<Unit> seaOrLandMatch = water ? Matches.UnitIsSea : Matches.UnitIsLand; placeableUnits .addAll(Match.getMatches(units, new CompositeMatchAnd<>(seaOrLandMatch, Matches.UnitIsNotConstruction))); if (!water) { placeableUnits.addAll( Match.getMatches(units, new CompositeMatchAnd<>(Matches.UnitIsAir, Matches.UnitIsNotConstruction))); } else if (((isBid || canProduceFightersOnCarriers() || AirThatCantLandUtil.isLHTRCarrierProduction(getData())) && Match.someMatch(allProducedUnits, Matches.UnitIsCarrier)) || ((isBid || canProduceNewFightersOnOldCarriers() || AirThatCantLandUtil.isLHTRCarrierProduction(getData())) && Match.someMatch(to.getUnits().getUnits(), Matches.UnitIsCarrier))) { placeableUnits.addAll( Match.getMatches(units, new CompositeMatchAnd<>(Matches.UnitIsAir, Matches.UnitCanLandOnCarrier))); } } if (Match.someMatch(units, Matches.UnitIsConstruction)) { final IntegerMap<String> constructionsMap = howManyOfEachConstructionCanPlace(to, to, units, player); final Collection<Unit> skipUnits = new ArrayList<>(); for (final Unit currentUnit : Match.getMatches(units, Matches.UnitIsConstruction)) { final int maxUnits = howManyOfConstructionUnit(currentUnit, constructionsMap); if (maxUnits > 0) { // we are doing this because we could have multiple unitTypes with the same constructionType, so we have to be // able to place the // max placement by constructionType of each unitType if (skipUnits.contains(currentUnit)) { continue; } placeableUnits.addAll(Match.getNMatches(units, maxUnits, Matches.unitIsOfType(currentUnit.getType()))); skipUnits.addAll(Match.getMatches(units, Matches.unitIsOfType(currentUnit.getType()))); } } } // remove any units that require other units to be consumed on creation, if we don't have enough to consume (veqryn) if (Match.someMatch(placeableUnits, Matches.UnitConsumesUnitsOnCreation)) { final Collection<Unit> unitsWhichConsume = Match.getMatches(placeableUnits, Matches.UnitConsumesUnitsOnCreation); for (final Unit unit : unitsWhichConsume) { if (Matches.UnitWhichConsumesUnitsHasRequiredUnits(unitsAtStartOfTurnInTO).invert().match(unit)) { placeableUnits.remove(unit); } } } // now check stacking limits final Collection<Unit> placeableUnits2 = new ArrayList<>(); final Collection<UnitType> typesAlreadyChecked = new ArrayList<>(); for (final Unit currentUnit : placeableUnits) { final UnitType ut = currentUnit.getType(); if (typesAlreadyChecked.contains(ut)) { continue; } typesAlreadyChecked.add(ut); placeableUnits2 .addAll(Match.getNMatches(placeableUnits, UnitAttachment.getMaximumNumberOfThisUnitTypeToReachStackingLimit( "placementLimit", ut, to, player, getData()), Matches.unitIsOfType(ut))); } if (!isUnitPlacementRestrictions()) { return placeableUnits2; } final Collection<Unit> placeableUnits3 = new ArrayList<>(); for (final Unit currentUnit : placeableUnits2) { final UnitAttachment ua = UnitAttachment.get(currentUnit.getUnitType()); // Can be null! final TerritoryAttachment ta = TerritoryAttachment.get(to); if (ua.getCanOnlyBePlacedInTerritoryValuedAtX() != -1 && ua.getCanOnlyBePlacedInTerritoryValuedAtX() > (ta == null ? 0 : ta.getProduction())) { continue; } if (unitWhichRequiresUnitsHasRequiredUnits(to, false).invert().match(currentUnit)) { continue; } if (Matches.UnitCanOnlyPlaceInOriginalTerritories.match(currentUnit) && !Matches.TerritoryIsOriginallyOwnedBy(player).match(to)) { continue; } // account for any unit placement restrictions by territory final String[] terrs = ua.getUnitPlacementRestrictions(); final Collection<Territory> listedTerrs = getListedTerritories(terrs); if (!listedTerrs.contains(to)) { placeableUnits3.add(currentUnit); } } return placeableUnits3; } protected boolean canWeConsumeUnits(final Collection<Unit> units, final Territory to, final boolean actuallyDoIt, final CompositeChange change) { boolean weCanConsume = true; final Collection<Unit> unitsAtStartOfTurnInTO = unitsAtStartOfStepInTerritory(to); final Collection<Unit> removedUnits = new ArrayList<>(); final Collection<Unit> unitsWhichConsume = Match.getMatches(units, Matches.UnitConsumesUnitsOnCreation); for (final Unit unit : unitsWhichConsume) { if (Matches.UnitWhichConsumesUnitsHasRequiredUnits(unitsAtStartOfTurnInTO).invert().match(unit)) { weCanConsume = false; } if (!weCanConsume) { break; } // remove units which are now consumed, then test the rest of the consuming units on the diminishing pile of units // which were in the // territory at start of turn final UnitAttachment ua = UnitAttachment.get(unit.getType()); final IntegerMap<UnitType> requiredUnitsMap = ua.getConsumesUnits(); final Collection<UnitType> requiredUnits = requiredUnitsMap.keySet(); for (final UnitType ut : requiredUnits) { final int requiredNumber = requiredUnitsMap.getInt(ut); final Match<Unit> unitIsOwnedByAndOfTypeAndNotDamaged = new CompositeMatchAnd<>( Matches.unitIsOwnedBy(unit.getOwner()), Matches.unitIsOfType(ut), Matches.UnitHasNotTakenAnyBombingUnitDamage, Matches.UnitHasNotTakenAnyDamage, Matches.UnitIsNotDisabled); final Collection<Unit> unitsBeingRemoved = Match.getNMatches(unitsAtStartOfTurnInTO, requiredNumber, unitIsOwnedByAndOfTypeAndNotDamaged); unitsAtStartOfTurnInTO.removeAll(unitsBeingRemoved); // if we should actually do it, not just test, then add to bridge if (actuallyDoIt && change != null) { final Change remove = ChangeFactory.removeUnits(to, unitsBeingRemoved); change.add(remove); removedUnits.addAll(unitsBeingRemoved); } } } if (weCanConsume && actuallyDoIt && change != null && !change.isEmpty()) { m_bridge.getHistoryWriter().startEvent( "Units in " + to.getName() + " being upgraded or consumed: " + MyFormatter.unitsToTextNoOwner(removedUnits), removedUnits); } return weCanConsume; } /** * Returns -1 if can place unlimited units. */ protected int getMaxUnitsToBePlaced(final Collection<Unit> units, final Territory to, final PlayerID player, final boolean countSwitchedProductionToNeighbors) { final IntegerMap<Territory> map = getMaxUnitsToBePlacedMap(units, to, player, countSwitchedProductionToNeighbors); int production = 0; for (final Entry<Territory, Integer> entry : map.entrySet()) { final int prodT = entry.getValue(); if (prodT == -1) { return -1; } production += prodT; } return production; } /** * Returns -1 somewhere in the map if can place unlimited units. */ protected IntegerMap<Territory> getMaxUnitsToBePlacedMap(final Collection<Unit> units, final Territory to, final PlayerID player, final boolean countSwitchedProductionToNeighbors) { final IntegerMap<Territory> rVal = new IntegerMap<>(); final List<Territory> producers = getAllProducers(to, player, units); if (producers.isEmpty()) { return rVal; } Collections.sort(producers, getBestProducerComparator(to, units, player)); final Collection<Territory> notUsableAsOtherProducers = new ArrayList<>(); notUsableAsOtherProducers.addAll(producers); final Map<Territory, Integer> currentAvailablePlacementForOtherProducers = new HashMap<>(); for (final Territory producerTerritory : producers) { final Collection<Unit> unitsCanBePlacedByThisProducer = (isUnitPlacementRestrictions() ? Match.getMatches(units, unitWhichRequiresUnitsHasRequiredUnits(producerTerritory, true)) : new ArrayList<>(units)); final int prodT = getMaxUnitsToBePlacedFrom(producerTerritory, unitsCanBePlacedByThisProducer, to, player, countSwitchedProductionToNeighbors, notUsableAsOtherProducers, currentAvailablePlacementForOtherProducers); rVal.put(producerTerritory, prodT); } return rVal; } /** * Returns -1 if can place unlimited units. */ protected int getMaxUnitsToBePlacedFrom(final Territory producer, final Collection<Unit> units, final Territory to, final PlayerID player) { return getMaxUnitsToBePlacedFrom(producer, units, to, player, false, null, null); } /** * Returns -1 if can place unlimited units. */ protected int getMaxUnitsToBePlacedFrom(final Territory producer, final Collection<Unit> units, final Territory to, final PlayerID player, final boolean countSwitchedProductionToNeighbors, final Collection<Territory> notUsableAsOtherProducers, final Map<Territory, Integer> currentAvailablePlacementForOtherProducers) { // we may have special units with requiresUnits restrictions final Collection<Unit> unitsCanBePlacedByThisProducer = (isUnitPlacementRestrictions() ? Match.getMatches(units, unitWhichRequiresUnitsHasRequiredUnits(producer, true)) : new ArrayList<>(units)); if (unitsCanBePlacedByThisProducer.size() <= 0) { return 0; } // if its an original factory then unlimited production // Can be null! final TerritoryAttachment ta = TerritoryAttachment.get(producer); final CompositeMatchAnd<Unit> factoryMatch = new CompositeMatchAnd<>( Matches.UnitIsOwnedAndIsFactoryOrCanProduceUnits(player), Matches.unitIsBeingTransported().invert()); if (producer.isWater()) { factoryMatch.add(Matches.UnitIsLand.invert()); } else { factoryMatch.add(Matches.UnitIsSea.invert()); } final Collection<Unit> factoryUnits = producer.getUnits().getMatches(factoryMatch); // boolean placementRestrictedByFactory = isPlacementRestrictedByFactory(); final boolean unitPlacementPerTerritoryRestricted = isUnitPlacementPerTerritoryRestricted(); final boolean originalFactory = (ta != null && ta.getOriginalFactory()); final boolean playerIsOriginalOwner = factoryUnits.size() > 0 && m_player.equals(getOriginalFactoryOwner(producer)); final RulesAttachment ra = (RulesAttachment) player.getAttachment(Constants.RULES_ATTACHMENT_NAME); final Collection<Unit> alreadProducedUnits = getAlreadyProduced(producer); final int unitCountAlreadyProduced = alreadProducedUnits.size(); if (originalFactory && playerIsOriginalOwner) { if (ra != null && ra.getMaxPlacePerTerritory() != -1) { return Math.max(0, ra.getMaxPlacePerTerritory() - unitCountAlreadyProduced); } return -1; } // Restricts based on the STARTING number of units in a territory (otherwise it is infinite placement) if (unitPlacementPerTerritoryRestricted) { if (ra != null && ra.getPlacementPerTerritory() > 0) { final int allowedPlacement = ra.getPlacementPerTerritory(); final int ownedUnitsInTerritory = Match.countMatches(to.getUnits().getUnits(), Matches.unitIsOwnedBy(player)); if (ownedUnitsInTerritory >= allowedPlacement) { return 0; } if (ra.getMaxPlacePerTerritory() == -1) { return -1; } return Math.max(0, ra.getMaxPlacePerTerritory() - unitCountAlreadyProduced); } } // a factory can produce the same number of units as the number of PUs the territory generates each turn (or not, if // it has // canProduceXUnits) int production = 0; // int territoryValue = getProduction(producer); final int maxConstructions = howManyOfEachConstructionCanPlace(to, producer, unitsCanBePlacedByThisProducer, player).totalValues(); final boolean wasFactoryThereAtStart = wasOwnedUnitThatCanProduceUnitsOrIsFactoryInTerritoryAtStartOfStep(producer, player); // If there's NO factory, allow placement of the factory if (!wasFactoryThereAtStart) { if (ra != null && ra.getMaxPlacePerTerritory() > 0) { return Math.max(0, Math.min(maxConstructions, ra.getMaxPlacePerTerritory() - unitCountAlreadyProduced)); } return Math.max(0, maxConstructions); } // getHowMuchCanUnitProduce accounts for IncreasedFactoryProduction, but does not account for maxConstructions production = TripleAUnit.getProductionPotentialOfTerritory(unitsAtStartOfStepInTerritory(producer), producer, player, getData(), true, true); // increase the production by the number of constructions allowed if (maxConstructions > 0) { production += maxConstructions; } // return 0 if less than 0 if (production < 0) { return 0; } production += Match.countMatches(alreadProducedUnits, Matches.UnitIsConstruction); // Now we check if units we have already produced here could be produced by a different producer int unitCountHaveToAndHaveBeenBeProducedHere = unitCountAlreadyProduced; if (countSwitchedProductionToNeighbors && unitCountAlreadyProduced > 0) { if (notUsableAsOtherProducers == null) { throw new IllegalStateException( "notUsableAsOtherProducers cannot be null if countSwitchedProductionToNeighbors is true"); } if (currentAvailablePlacementForOtherProducers == null) { throw new IllegalStateException( "currentAvailablePlacementForOtherProducers cannot be null if countSwitchedProductionToNeighbors is true"); } int productionCanNotBeMoved = 0; int productionThatCanBeTakenOver = 0; // try to find a placement move (to an adjacent sea zone) that can be taken over by some other territory factory for (final UndoablePlacement placementMove : m_placements) { if (placementMove.getProducerTerritory().equals(producer)) { final Territory placeTerritory = placementMove.getPlaceTerritory(); final Collection<Unit> unitsPlacedByCurrentPlacementMove = placementMove.getUnits(); // TODO: Units which have the unit attachment property, requiresUnits, are too difficult to mess with // logically, so we ignore them // for our special 'move shit around' methods. if (!placeTerritory.isWater() || (isUnitPlacementRestrictions() && Match.someMatch(unitsPlacedByCurrentPlacementMove, Matches.UnitRequiresUnitsOnCreation))) { productionCanNotBeMoved += unitsPlacedByCurrentPlacementMove.size(); } else { final int maxProductionThatCanBeTakenOverFromThisPlacement = unitsPlacedByCurrentPlacementMove.size(); int productionThatCanBeTakenOverFromThisPlacement = 0; // find other producers for this placement move to the same water territory final List<Territory> newPotentialOtherProducers = getAllProducers(placeTerritory, player, unitsCanBePlacedByThisProducer); newPotentialOtherProducers.removeAll(notUsableAsOtherProducers); Collections.sort(newPotentialOtherProducers, getBestProducerComparator(placeTerritory, unitsCanBePlacedByThisProducer, player)); for (final Territory potentialOtherProducer : newPotentialOtherProducers) { Integer potential = currentAvailablePlacementForOtherProducers.get(potentialOtherProducer); if (potential == null) { potential = getMaxUnitsToBePlacedFrom(potentialOtherProducer, unitsPlacedInTerritorySoFar(placeTerritory), placeTerritory, player); } if (potential == -1) { currentAvailablePlacementForOtherProducers.put(potentialOtherProducer, potential); productionThatCanBeTakenOverFromThisPlacement = maxProductionThatCanBeTakenOverFromThisPlacement; break; } else { final int needed = maxProductionThatCanBeTakenOverFromThisPlacement - productionThatCanBeTakenOverFromThisPlacement; final int surplus = potential - needed; if (surplus > 0) { currentAvailablePlacementForOtherProducers.put(potentialOtherProducer, surplus); productionThatCanBeTakenOverFromThisPlacement += needed; } else { currentAvailablePlacementForOtherProducers.put(potentialOtherProducer, 0); productionThatCanBeTakenOverFromThisPlacement += potential; notUsableAsOtherProducers.add(potentialOtherProducer); } if (surplus >= 0) { break; } } } if (productionThatCanBeTakenOverFromThisPlacement > maxProductionThatCanBeTakenOverFromThisPlacement) { throw new IllegalStateException("productionThatCanBeTakenOverFromThisPlacement should never be larger " + "than maxProductionThatCanBeTakenOverFromThisPlacement"); } productionThatCanBeTakenOver += productionThatCanBeTakenOverFromThisPlacement; } if (productionThatCanBeTakenOver >= unitCountAlreadyProduced - productionCanNotBeMoved) { break; } } } unitCountHaveToAndHaveBeenBeProducedHere = Math.max(0, unitCountAlreadyProduced - productionThatCanBeTakenOver); } if (ra != null && ra.getMaxPlacePerTerritory() > 0) { return Math.max(0, Math.min(production - unitCountHaveToAndHaveBeenBeProducedHere, ra.getMaxPlacePerTerritory() - unitCountHaveToAndHaveBeenBeProducedHere)); } return Math.max(0, production - unitCountHaveToAndHaveBeenBeProducedHere); } /** * @return gets the production of the territory. */ protected int getProduction(final Territory territory) { // Can be null! final TerritoryAttachment ta = TerritoryAttachment.get(territory); if (ta != null) { return ta.getProduction(); } return 0; } /** * @param to * referring territory. * @param units * units to place * @param player * PlayerID * @return an empty IntegerMap if you can't produce any constructions (will never return null) */ public IntegerMap<String> howManyOfEachConstructionCanPlace(final Territory to, final Territory producer, final Collection<Unit> units, final PlayerID player) { // constructions can ONLY be produced BY the same territory that they are going into! if (!to.equals(producer) || units == null || units.isEmpty() || !Match.someMatch(units, Matches.UnitIsConstruction)) { return new IntegerMap<>(); } final Collection<Unit> unitsAtStartOfTurnInTO = unitsAtStartOfStepInTerritory(to); final Collection<Unit> unitsInTO = to.getUnits().getUnits(); final Collection<Unit> unitsPlacedAlready = getAlreadyProduced(to); // build an integer map of each unit we have in our list of held units, as well as integer maps for maximum units // and units per turn final IntegerMap<String> unitMapHeld = new IntegerMap<>(); final IntegerMap<String> unitMapMaxType = new IntegerMap<>(); final IntegerMap<String> unitMapTypePerTurn = new IntegerMap<>(); final int maxFactory = games.strategy.triplea.Properties.getFactoriesPerCountry(getData()); final Iterator<Unit> unitHeldIter = Match.getMatches(units, Matches.UnitIsConstruction).iterator(); // Can be null! final TerritoryAttachment terrAttachment = TerritoryAttachment.get(to); int toProduction = 0; if (terrAttachment != null) { toProduction = terrAttachment.getProduction(); } while (unitHeldIter.hasNext()) { final Unit currentUnit = unitHeldIter.next(); final UnitAttachment ua = UnitAttachment.get(currentUnit.getUnitType()); // account for any unit placement restrictions by territory if (isUnitPlacementRestrictions()) { final String[] terrs = ua.getUnitPlacementRestrictions(); final Collection<Territory> listedTerrs = getListedTerritories(terrs); if (listedTerrs.contains(to)) { continue; } if (ua.getCanOnlyBePlacedInTerritoryValuedAtX() != -1 && ua.getCanOnlyBePlacedInTerritoryValuedAtX() > toProduction) { continue; } if (unitWhichRequiresUnitsHasRequiredUnits(to, false).invert().match(currentUnit)) { continue; } } // remove any units that require other units to be consumed on creation (veqryn) if (Matches.UnitConsumesUnitsOnCreation.match(currentUnit) && Matches.UnitWhichConsumesUnitsHasRequiredUnits(unitsAtStartOfTurnInTO).invert().match(currentUnit)) { continue; } unitMapHeld.add(ua.getConstructionType(), 1); unitMapTypePerTurn.put(ua.getConstructionType(), ua.getConstructionsPerTerrPerTypePerTurn()); if (ua.getConstructionType().equals(Constants.CONSTRUCTION_TYPE_FACTORY)) { unitMapMaxType.put(ua.getConstructionType(), maxFactory); } else { unitMapMaxType.put(ua.getConstructionType(), ua.getMaxConstructionsPerTypePerTerr()); } } final boolean moreWithoutFactory = games.strategy.triplea.Properties.getMoreConstructionsWithoutFactory(getData()); final boolean moreWithFactory = games.strategy.triplea.Properties.getMoreConstructionsWithFactory(getData()); final boolean unlimitedConstructions = games.strategy.triplea.Properties.getUnlimitedConstructions(getData()); final boolean wasFactoryThereAtStart = wasOwnedUnitThatCanProduceUnitsOrIsFactoryInTerritoryAtStartOfStep(to, player); // build an integer map of each construction unit in the territory final IntegerMap<String> unitMapTO = new IntegerMap<>(); if (Match.someMatch(unitsInTO, Matches.UnitIsConstruction)) { for (final Unit currentUnit : Match.getMatches(unitsInTO, Matches.UnitIsConstruction)) { final UnitAttachment ua = UnitAttachment.get(currentUnit.getUnitType()); /* * if (Matches.UnitIsFactory.match(currentUnit) && !ua.getIsConstruction()) * unitMapTO.add("factory", 1); * else */ unitMapTO.add(ua.getConstructionType(), 1); } // account for units already in the territory, based on max final Iterator<String> mapString = unitMapHeld.keySet().iterator(); while (mapString.hasNext()) { final String constructionType = mapString.next(); int unitMax = unitMapMaxType.getInt(constructionType); if (wasFactoryThereAtStart && !constructionType.equals(Constants.CONSTRUCTION_TYPE_FACTORY) && !constructionType.endsWith("structure")) { unitMax = Math.max(Math.max(unitMax, (moreWithFactory ? toProduction : 0)), (unlimitedConstructions ? 10000 : 0)); } if (!wasFactoryThereAtStart && !constructionType.equals(Constants.CONSTRUCTION_TYPE_FACTORY) && !constructionType.endsWith("structure")) { unitMax = Math.max(Math.max(unitMax, (moreWithoutFactory ? toProduction : 0)), (unlimitedConstructions ? 10000 : 0)); } unitMapHeld.put(constructionType, Math.max(0, Math.min(unitMax - unitMapTO.getInt(constructionType), unitMapHeld.getInt(constructionType)))); } } // deal with already placed units final Iterator<Unit> unitAlready = Match.getMatches(unitsPlacedAlready, Matches.UnitIsConstruction).iterator(); while (unitAlready.hasNext()) { final Unit currentUnit = unitAlready.next(); final UnitAttachment ua = UnitAttachment.get(currentUnit.getUnitType()); unitMapTypePerTurn.add(ua.getConstructionType(), -1); } // modify this list based on how many we can place per turn final IntegerMap<String> unitsAllowed = new IntegerMap<>(); final Iterator<String> mapString2 = unitMapHeld.keySet().iterator(); while (mapString2.hasNext()) { final String constructionType = mapString2.next(); final int unitAllowed = Math.max(0, Math.min(unitMapTypePerTurn.getInt(constructionType), unitMapHeld.getInt(constructionType))); if (unitAllowed > 0) { unitsAllowed.put(constructionType, unitAllowed); } } // return our integer map return unitsAllowed; } public int howManyOfConstructionUnit(final Unit unit, final IntegerMap<String> constructionsMap) { final UnitAttachment ua = UnitAttachment.get(unit.getUnitType()); if (/* !ua.getIsFactory() && */(!ua.getIsConstruction() || ua.getConstructionsPerTerrPerTypePerTurn() < 1 || ua.getMaxConstructionsPerTypePerTerr() < 1)) { return 0; } return Math.max(0, constructionsMap.getInt(ua.getConstructionType())); } /** * @param to * - Territory we are testing for required units * @param doNotCountNeighbors * - If false, and 'to' is water, then we will test neighboring land territories to see if they have any of the * required units as * well. * @return - Whether the territory contains one of the required combos of units * (and if 'doNotCountNeighbors' is false, and unit is Sea unit, will return true if an adjacent land * territory has one of the * required combos as well). */ public Match<Unit> unitWhichRequiresUnitsHasRequiredUnits(final Territory to, final boolean doNotCountNeighbors) { return new Match<Unit>() { @Override public boolean match(final Unit unitWhichRequiresUnits) { if (!Matches.UnitRequiresUnitsOnCreation.match(unitWhichRequiresUnits)) { return true; } final Collection<Unit> unitsAtStartOfTurnInProducer = unitsAtStartOfStepInTerritory(to); // do not need to remove unowned here, as this match will remove unowned units from consideration. if (Matches.UnitWhichRequiresUnitsHasRequiredUnitsInList(unitsAtStartOfTurnInProducer) .match(unitWhichRequiresUnits)) { return true; } if (!doNotCountNeighbors) { if (Matches.UnitIsSea.match(unitWhichRequiresUnits)) { for (final Territory current : getAllProducers(to, m_player, Collections.singletonList(unitWhichRequiresUnits), true)) { final Collection<Unit> unitsAtStartOfTurnInCurrent = unitsAtStartOfStepInTerritory(current); if (Matches.UnitWhichRequiresUnitsHasRequiredUnitsInList(unitsAtStartOfTurnInCurrent) .match(unitWhichRequiresUnits)) { return true; } } } } return false; } }; } public boolean getCanAllUnitsWithRequiresUnitsBePlacedCorrectly(final Collection<Unit> units, final Territory to) { if (!isUnitPlacementRestrictions() || !Match.someMatch(units, Matches.UnitRequiresUnitsOnCreation)) { return true; } final IntegerMap<Territory> producersMap = getMaxUnitsToBePlacedMap(units, to, m_player, true); final List<Territory> producers = getAllProducers(to, m_player, units); if (producers.isEmpty()) { return false; } Collections.sort(producers, getBestProducerComparator(to, units, m_player)); final Collection<Unit> unitsLeftToPlace = new ArrayList<>(units); for (final Territory t : producers) { if (unitsLeftToPlace.isEmpty()) { return true; } final int productionHere = producersMap.getInt(t); final List<Unit> canBePlacedHere = Match.getMatches(unitsLeftToPlace, unitWhichRequiresUnitsHasRequiredUnits(t, true)); if (productionHere == -1 || productionHere >= canBePlacedHere.size()) { unitsLeftToPlace.removeAll(canBePlacedHere); continue; } Collections.sort(canBePlacedHere, getHardestToPlaceWithRequiresUnitsRestrictions(true)); final Collection<Unit> placedHere = Match.getNMatches(canBePlacedHere, productionHere, Match.getAlwaysMatch()); unitsLeftToPlace.removeAll(placedHere); } return unitsLeftToPlace.isEmpty(); } protected Comparator<Territory> getBestProducerComparator(final Territory to, final Collection<Unit> units, final PlayerID player) { return (t1, t2) -> { if (t1 == t2 || t1.equals(t2)) { return 0; } // producing to territory comes first if (to == t1 || to.equals(t1)) { return -1; } else if (to == t2 || to.equals(t2)) { return 1; } final int left1 = getMaxUnitsToBePlacedFrom(t1, units, to, player); final int left2 = getMaxUnitsToBePlacedFrom(t2, units, to, player); if (left1 == left2) { return 0; } // production of -1 == infinite if (left1 == -1) { return -1; } if (left2 == -1) { return 1; } if (left1 > left2) { return -1; } return 1; }; } protected Comparator<Unit> getUnitConstructionComparator() { return (u1, u2) -> { final boolean construction1 = Matches.UnitIsConstruction.match(u1); final boolean construction2 = Matches.UnitIsConstruction.match(u2); if (construction1 == construction2) { return 0; } else if (construction1) { return -1; } else { return 1; } }; } protected Comparator<Unit> getHardestToPlaceWithRequiresUnitsRestrictions(final boolean sortConstructionsToFront) { return (u1, u2) -> { if (u1 == u2 || u1.equals(u2)) { return 0; } final UnitAttachment ua1 = UnitAttachment.get(u1.getType()); final UnitAttachment ua2 = UnitAttachment.get(u2.getType()); if (ua1 == null && ua2 == null) { return 0; } if (ua1 != null && ua2 == null) { return -1; } if (ua1 == null && ua2 != null) { return 1; } // constructions go ahead first if (sortConstructionsToFront) { final int constructionSort = getUnitConstructionComparator().compare(u1, u2); if (constructionSort != 0) { return constructionSort; } } final ArrayList<String[]> ru1 = ua1.getRequiresUnits(); final ArrayList<String[]> ru2 = ua2.getRequiresUnits(); final int rus1 = (ru1 == null ? Integer.MAX_VALUE : (ru1.isEmpty() ? Integer.MAX_VALUE : ru1.size())); final int rus2 = (ru2 == null ? Integer.MAX_VALUE : (ru2.isEmpty() ? Integer.MAX_VALUE : ru2.size())); if (rus1 == rus2) { return 0; } // fewer means more difficult, and more difficult goes to front of list. if (rus1 < rus2) { return -1; } return 1; }; } /** * @param to * referring territory. * @return collection of units that were there at start of turn */ public Collection<Unit> unitsAtStartOfStepInTerritory(final Territory to) { if (to == null) { return new ArrayList<>(); } final Collection<Unit> unitsInTO = to.getUnits().getUnits(); final Collection<Unit> unitsPlacedAlready = getAlreadyProduced(to); if (Matches.TerritoryIsWater.match(to)) { for (final Territory current : getAllProducers(to, m_player, null, true)) { unitsPlacedAlready.addAll(getAlreadyProduced(current)); } } final Collection<Unit> unitsAtStartOfTurnInTO = new ArrayList<>(unitsInTO); unitsAtStartOfTurnInTO.removeAll(unitsPlacedAlready); return unitsAtStartOfTurnInTO; } public Collection<Unit> unitsPlacedInTerritorySoFar(final Territory to) { if (to == null) { return new ArrayList<>(); } final Collection<Unit> unitsInTO = to.getUnits().getUnits(); final Collection<Unit> unitsAtStartOfStep = unitsAtStartOfStepInTerritory(to); unitsInTO.removeAll(unitsAtStartOfStep); return unitsInTO; } /** * @param to * referring territory. * @param player * PlayerID * @return whether there was an owned unit capable of producing, in this territory at the start of this phase/step */ public boolean wasOwnedUnitThatCanProduceUnitsOrIsFactoryInTerritoryAtStartOfStep(final Territory to, final PlayerID player) { final Collection<Unit> unitsAtStartOfTurnInTO = unitsAtStartOfStepInTerritory(to); final CompositeMatchAnd<Unit> factoryMatch = new CompositeMatchAnd<>( Matches.UnitIsOwnedAndIsFactoryOrCanProduceUnits(player), Matches.unitIsBeingTransported().invert()); // land factories in water can't produce, and sea factories in land can't produce. air can produce like land if in // land, and like sea if // in water. if (to.isWater()) { factoryMatch.add(Matches.UnitIsLand.invert()); } else { factoryMatch.add(Matches.UnitIsSea.invert()); } return Match.countMatches(unitsAtStartOfTurnInTO, factoryMatch) > 0; } /** * There must be a factory in the territory or an illegal state exception * will be thrown. return value may be null. */ protected PlayerID getOriginalFactoryOwner(final Territory territory) { final Collection<Unit> factoryUnits = territory.getUnits().getMatches(Matches.UnitCanProduceUnits); if (factoryUnits.size() == 0) { throw new IllegalStateException("No factory in territory:" + territory); } final Iterator<Unit> iter = factoryUnits.iterator(); // final GameData data = getData(); while (iter.hasNext()) { final Unit factory2 = iter.next(); if (m_player.equals(OriginalOwnerTracker.getOriginalOwner(factory2))) { return OriginalOwnerTracker.getOriginalOwner(factory2); } } final Unit factory = factoryUnits.iterator().next(); // return DelegateFinder.battleDelegate(data).getOriginalOwnerTracker().getOriginalOwner(factory); return OriginalOwnerTracker.getOriginalOwner(factory); } /** * The rule is that new fighters can be produced on new carriers. This does * not allow for fighters to be produced on old carriers. */ protected String validateNewAirCanLandOnCarriers(final Territory to, final Collection<Unit> units) { final int cost = AirMovementValidator.carrierCost(units); int capacity = AirMovementValidator.carrierCapacity(units, to); capacity += AirMovementValidator.carrierCapacity(to.getUnits().getUnits(), to); if (cost > capacity) { return "Not enough new carriers to land all the fighters"; } return null; } /** * Get what air units must move before the end of the players turn. * * @return a list of Territories with air units that must move */ @Override public Collection<Territory> getTerritoriesWhereAirCantLand() { return new AirThatCantLandUtil(m_bridge).getTerritoriesWhereAirCantLand(m_player); } protected boolean canProduceFightersOnCarriers() { return games.strategy.triplea.Properties.getProduceFightersOnCarriers(getData()); } protected boolean canProduceNewFightersOnOldCarriers() { return games.strategy.triplea.Properties.getProduceNewFightersOnOldCarriers(getData()); } protected boolean canMoveExistingFightersToNewCarriers() { return games.strategy.triplea.Properties.getMoveExistingFightersToNewCarriers(getData()); } protected boolean isWW2V2() { return games.strategy.triplea.Properties.getWW2V2(getData()); } protected boolean isUnitPlacementInEnemySeas() { return games.strategy.triplea.Properties.getUnitPlacementInEnemySeas(getData()); } protected boolean wasConquered(final Territory t) { final BattleTracker tracker = DelegateFinder.battleDelegate(getData()).getBattleTracker(); return tracker.wasConquered(t); } protected boolean isPlaceInAnyTerritory() { return games.strategy.triplea.Properties.getPlaceInAnyTerritory(getData()); } protected boolean isUnitPlacementPerTerritoryRestricted() { return games.strategy.triplea.Properties.getUnitPlacementPerTerritoryRestricted(getData()); } protected boolean isUnitPlacementRestrictions() { return games.strategy.triplea.Properties.getUnitPlacementRestrictions(getData()); } protected List<Territory> getAllProducers(final Territory to, final PlayerID player, final Collection<Unit> unitsToPlace) { return getAllProducers(to, player, unitsToPlace, false); } protected boolean isPlayerAllowedToPlacementAnyTerritoryOwnedLand(final PlayerID player) { if (isPlaceInAnyTerritory()) { final RulesAttachment ra = (RulesAttachment) player.getAttachment(Constants.RULES_ATTACHMENT_NAME); if (ra != null && ra.getPlacementAnyTerritory()) { return true; } } return false; } protected boolean isPlayerAllowedToPlacementAnySeaZoneByOwnedLand(final PlayerID player) { if (isPlaceInAnyTerritory()) { final RulesAttachment ra = (RulesAttachment) player.getAttachment(Constants.RULES_ATTACHMENT_NAME); if (ra != null && ra.getPlacementAnySeaZone()) { return true; } } return false; } protected boolean isPlacementAllowedInCapturedTerritory(final PlayerID player) { final RulesAttachment ra = (RulesAttachment) player.getAttachment(Constants.RULES_ATTACHMENT_NAME); return ra != null && ra.getPlacementCapturedTerritory(); } protected boolean isPlacementInCapitalRestricted(final PlayerID player) { final RulesAttachment ra = (RulesAttachment) player.getAttachment(Constants.RULES_ATTACHMENT_NAME); return ra != null && ra.getPlacementInCapitalRestricted(); } protected Collection<Territory> getListedTerritories(final String[] list) { final List<Territory> rVal = new ArrayList<>(); if (list == null) { return rVal; } for (final String name : list) { // Validate all territories exist final Territory territory = getData().getMap().getTerritory(name); if (territory == null) { throw new IllegalStateException("Rules & Conditions: No territory called:" + name); } rVal.add(territory); } return rVal; } @Override public Class<? extends IRemote> getRemoteType() { return IAbstractPlaceDelegate.class; } }