Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve Transports To Load Default #647

Merged
merged 10 commits into from May 7, 2016
13 changes: 0 additions & 13 deletions src/games/strategy/triplea/delegate/MoveValidator.java
Expand Up @@ -935,19 +935,6 @@ public static boolean hasNeutralBeforeEnd(final Route route) {
return route.hasNeutralBeforeEnd();
}

public static int getTransportCost(final Collection<Unit> units) {
if (units == null) {
return 0;
}
int cost = 0;
final Iterator<Unit> iter = units.iterator();
while (iter.hasNext()) {
final Unit item = iter.next();
cost += UnitAttachment.get(item.getType()).getTransportCost();
}
return cost;
}

private static Collection<Unit> getUnitsThatCantGoOnWater(final Collection<Unit> units) {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved this method to TransportUtils.

final Collection<Unit> retUnits = new ArrayList<Unit>();
for (final Unit unit : units) {
Expand Down
16 changes: 9 additions & 7 deletions src/games/strategy/triplea/delegate/TransportTracker.java
@@ -1,11 +1,5 @@
package games.strategy.triplea.delegate;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;

import games.strategy.engine.data.Change;
import games.strategy.engine.data.ChangeFactory;
import games.strategy.engine.data.CompositeChange;
Expand All @@ -16,14 +10,22 @@
import games.strategy.triplea.Constants;
import games.strategy.triplea.TripleAUnit;
import games.strategy.triplea.attachments.UnitAttachment;
import games.strategy.triplea.util.TransportUtils;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;

/**
* Tracks which transports are carrying which units. Also tracks the capacity
* that has been unloaded. To reset the unloaded call clearUnloadedCapacity().
*/
public class TransportTracker {

public static int getCost(final Collection<Unit> units) {
return MoveValidator.getTransportCost(units);
return TransportUtils.getTransportCost(units);
}

private static void assertTransport(final Unit u) {
Expand Down
3 changes: 2 additions & 1 deletion src/games/strategy/triplea/delegate/UnitComparator.java
Expand Up @@ -5,6 +5,7 @@
import games.strategy.engine.data.Unit;
import games.strategy.triplea.TripleAUnit;
import games.strategy.triplea.attachments.UnitAttachment;
import games.strategy.triplea.util.TransportUtils;
import games.strategy.util.IntegerMap;
import games.strategy.util.Match;

Expand Down Expand Up @@ -60,7 +61,7 @@ private static Comparator<Unit> getCapacityComparator(final List<Unit> transport
final IntegerMap<Unit> capacityMap = new IntegerMap<Unit>(transports.size() + 1, 1);
for (final Unit transport : transports) {
final Collection<Unit> transporting = TripleAUnit.get(transport).getTransporting();
capacityMap.add(transport, MoveValidator.getTransportCost(transporting));
capacityMap.add(transport, TransportUtils.getTransportCost(transporting));
}
return new Comparator<Unit>() {
@Override
Expand Down
5 changes: 3 additions & 2 deletions src/games/strategy/triplea/ui/EditPanel.java
Expand Up @@ -23,6 +23,7 @@
import games.strategy.triplea.delegate.UnitBattleComparator;
import games.strategy.triplea.delegate.dataObjects.MustMoveWithDetails;
import games.strategy.triplea.formatter.MyFormatter;
import games.strategy.triplea.util.TransportUtils;
import games.strategy.triplea.util.UnitSeperator;
import games.strategy.util.IntegerMap;
import games.strategy.util.Match;
Expand Down Expand Up @@ -550,8 +551,8 @@ public int compare(final Unit unit1, final Unit unit2) {
// Sort by decreasing transport capacity
final Collection<Unit> transporting1 = u1.getTransporting();
final Collection<Unit> transporting2 = u2.getTransporting();
final int cost1 = MoveValidator.getTransportCost(transporting1);
final int cost2 = MoveValidator.getTransportCost(transporting2);
final int cost1 = TransportUtils.getTransportCost(transporting1);
final int cost2 = TransportUtils.getTransportCost(transporting2);
if (cost1 != cost2) {
return cost2 - cost1;
}
Expand Down
8 changes: 4 additions & 4 deletions src/games/strategy/triplea/ui/MovePanel.java
Expand Up @@ -216,7 +216,7 @@ public boolean match(final Collection<Unit> units) {
final IntegerMap<Unit> capacityMap = new IntegerMap<Unit>();
for (final Unit transport : sortedTransports) {
final Collection<Unit> transporting = TripleAUnit.get(transport).getTransporting();
capacityMap.add(transport, MoveValidator.getTransportCost(transporting));
capacityMap.add(transport, TransportUtils.getTransportCost(transporting));
}
boolean hasChanged = false;
final Comparator<Unit> increasingCapacityComparator =
Expand Down Expand Up @@ -637,7 +637,7 @@ public boolean match(final Unit transport) {
capableTransports.removeAll(alliedTransports);
// First, load capable transports
final Map<Unit, Unit> unitsToCapableTransports =
TransportUtils.mapTransports(route, availableUnits, capableTransports);
TransportUtils.mapTransportsToLoadUsingMinTransports(availableUnits, capableTransports);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These 3 method updates are to hook up the new transport loading method that finds minimum number of needed transports.

for (final Unit unit : unitsToCapableTransports.keySet()) {
final Unit transport = unitsToCapableTransports.get(unit);
final int unitCost = UnitAttachment.get(unit.getType()).getTransportCost();
Expand All @@ -647,7 +647,7 @@ public boolean match(final Unit transport) {
availableUnits.removeAll(unitsToCapableTransports.keySet());
// Next, load allied transports
final Map<Unit, Unit> unitsToAlliedTransports =
TransportUtils.mapTransports(route, availableUnits, alliedTransports);
TransportUtils.mapTransportsToLoadUsingMinTransports(availableUnits, alliedTransports);
for (final Unit unit : unitsToAlliedTransports.keySet()) {
final Unit transport = unitsToAlliedTransports.get(unit);
final int unitCost = UnitAttachment.get(unit.getType()).getTransportCost();
Expand All @@ -661,7 +661,7 @@ public boolean match(final Unit transport) {
// are selected, since it may not be obvious
if (getSelectedEndpointTerritory() == null) {
final Map<Unit, Unit> unitsToIncapableTransports =
TransportUtils.mapTransports(route, availableUnits, incapableTransports);
TransportUtils.mapTransportsToLoadUsingMinTransports(availableUnits, incapableTransports);
for (final Unit unit : unitsToIncapableTransports.keySet()) {
final Unit transport = unitsToIncapableTransports.get(unit);
final int unitCost = UnitAttachment.get(unit.getType()).getTransportCost();
Expand Down
214 changes: 110 additions & 104 deletions src/games/strategy/triplea/util/TransportUtils.java
Expand Up @@ -7,7 +7,6 @@
import games.strategy.triplea.delegate.TransportTracker;
import games.strategy.util.IntegerMap;
import games.strategy.util.Match;
import games.strategy.util.Util;

import java.util.ArrayList;
import java.util.Collection;
Expand All @@ -17,16 +16,13 @@
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Optional;


public class TransportUtils {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The diff of this class is pretty much unreadable do to several methods being refactored and moved around. Suggest either looking over the final class or review commit by commit.


/**
* This method is static so it can be called from the client side.
*
* @return a map of unit -> transport (null if no mapping can be
* done either because there is not sufficient transport capacity or because
* a unit is not with its transport)
* Returns a map of unit -> transport.
*/
public static Map<Unit, Unit> mapTransports(final Route route, final Collection<Unit> units,
final Collection<Unit> transportsToLoad) {
Expand All @@ -40,125 +36,55 @@ public static Map<Unit, Unit> mapTransports(final Route route, final Collection<
}

/**
* Returns a map of unit -> transport. Unit must already be loaded in the
* transport. If no units are loaded in the transports then an empty Map will
* be returned.
* Returns a map of unit -> transport. Tries to load units evenly across all transports.
*/
private static Map<Unit, Unit> mapTransportsAlreadyLoaded(final Collection<Unit> units,
public static Map<Unit, Unit> mapTransportsToLoad(final Collection<Unit> units,
final Collection<Unit> transports) {
final Collection<Unit> canBeTransported = Match.getMatches(units, Matches.UnitCanBeTransported);
final Collection<Unit> canTransport = Match.getMatches(transports, Matches.UnitCanTransport);

final List<Unit> canBeTransported = sortByTransportCostDescending(units);
final List<Unit> canTransport = sortByTransportCapacityAscending(transports);

// Add units to transports evenly
final Map<Unit, Unit> mapping = new HashMap<Unit, Unit>();
final Iterator<Unit> land = canBeTransported.iterator();
while (land.hasNext()) {
final Unit currentTransported = land.next();
final Unit transport = TransportTracker.transportedBy(currentTransported);
final IntegerMap<Unit> addedLoad = new IntegerMap<Unit>();
for (final Unit unit : canBeTransported) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Complexity looks to be getting up there in this method. Can we push some of the lower level logic into helper methods? Java8 streams + filter operations might be another option, the inner most for loop looks like it can be simplified to a stream, filter, find_first operation

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Considered using streams but given the number of variables being tracked and needing to move the loaded transport to the end of the list, I couldn't think of a good way to do that.

I'll take a look to see if a helper method makes sense.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ended up creating a helper method. I think it helps out a little though it isn't the best helper method as it has 4 parameters and modifies 2 of them.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The helper method certainly helped

final Optional<Unit> transport = loadUnitIntoFirstAvailableTransport(unit, canTransport, mapping, addedLoad);

// already being transported, make sure it is in transports
if (transport == null) {
continue;
// Move loaded transport to end of list
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of moving the loaded transport to the end of the list, could we check if it is full, and if so, remove it?
Higher level though, even that may not be necessary. Everything here should be in-memory, so the method may run plenty fast without the optimization. I'm afraid it violated the least-surprise principle, it was curious to see an element be removed to be added again.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I originally had that in and decided to remove it since performance wasn't really an issue. Figured its better to keep it as simple as possible.

This is kind of the age old problem of having to edit a list you are looping through. Essentially after a unit is added, the transport needs to be moved to the end of the list. Any thoughts on a better way to move the loaded transport to the end of the list other than removing then adding it?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMO avoid the need to move it to the end. Alternatively have an index point where you start your iterations. This would mean converting your inner foreach loop into an old school style for( int i = startIndex, n = list.size(); i < n; i ++ ) {

if (transport.isPresent()) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suspect you can simplify, everything should be in memory, order nanosecond runtimes. This performance improvement strikes me each time.

canTransport.remove(transport.get());
canTransport.add(transport.get());
}
if (!canTransport.contains(transport)) {
continue;
}
mapping.put(currentTransported, transport);
}
return mapping;
}

/**
* Returns a map of unit -> transport. Tries to find transports to load all
* units. If it can't succeed returns an empty Map.
* Returns a map of unit -> transport. Tries load max units into each transport before moving to next.
*/
public static Map<Unit, Unit> mapTransportsToLoad(final Collection<Unit> units, final Collection<Unit> transports) {
final List<Unit> canBeTransported = Match.getMatches(units, Matches.UnitCanBeTransported);
int transportIndex = 0;
final Comparator<Unit> transportCostComparator = new Comparator<Unit>() {
@Override
public int compare(final Unit o1, final Unit o2) {
final int cost1 = UnitAttachment.get((o1).getUnitType()).getTransportCost();
final int cost2 = UnitAttachment.get((o2).getUnitType()).getTransportCost();
return cost2 - cost1;
}
};
public static Map<Unit, Unit> mapTransportsToLoadUsingMinTransports(final Collection<Unit> units,
final Collection<Unit> transports) {

// fill the units with the highest cost first which allows easy loading of 2 infantry and 2 tanks on 2 transports in
// WW2V2 rules
Collections.sort(canBeTransported, transportCostComparator);
final List<Unit> canTransport = Match.getMatches(transports, Matches.UnitCanTransport);
final Comparator<Unit> transportCapacityComparator = new Comparator<Unit>() {
@Override
public int compare(final Unit o1, final Unit o2) {
final int capacityLeft1 = TransportTracker.getAvailableCapacity(o1);
final int capacityLeft2 = TransportTracker.getAvailableCapacity(o1);
if (capacityLeft1 != capacityLeft2) {
return capacityLeft1 - capacityLeft2;
}
final int capacity1 = UnitAttachment.get((o1).getUnitType()).getTransportCapacity();
final int capacity2 = UnitAttachment.get((o2).getUnitType()).getTransportCapacity();
return capacity1 - capacity2;
}
};
final List<Unit> canBeTransported = sortByTransportCostDescending(units);
final List<Unit> canTransport = sortByTransportCapacityAscending(transports);

// fill transports with the lowest capacity first
Collections.sort(canTransport, transportCapacityComparator);
// Add max units to each transport
final Map<Unit, Unit> mapping = new HashMap<Unit, Unit>();
final IntegerMap<Unit> addedLoad = new IntegerMap<Unit>();
final Comparator<Unit> previouslyLoadedToLast = transportsThatPreviouslyUnloadedComeLast();
for (final Unit land : canBeTransported) {
final UnitAttachment landUA = UnitAttachment.get(land.getType());
final int cost = landUA.getTransportCost();
boolean loaded = false;

// we want to try to distribute units evenly to all the transports
// if the user has 2 infantry, and selects two transports to load
// we should put 1 infantry in each transport.
// the algorithm below does not guarantee even distribution in all cases
// but it solves most of the cases
final List<Unit> shiftedToEnd = Util.shiftElementsToEnd(canTransport, transportIndex);

// review the following loop in light of bug ticket 2827064- previously unloaded trns perhaps shouldn't be
// included.
Collections.sort(shiftedToEnd, previouslyLoadedToLast);
final Iterator<Unit> transportIter = shiftedToEnd.iterator();
while (transportIter.hasNext() && !loaded) {
transportIndex++;
if (transportIndex >= canTransport.size()) {
transportIndex = 0;
}
final Unit transport = transportIter.next();
int capacity = TransportTracker.getAvailableCapacity(transport);
capacity -= addedLoad.getInt(transport);
for (final Unit transport : canTransport) {
int capacity = TransportTracker.getAvailableCapacity(transport);
for (final Iterator<Unit> it = canBeTransported.iterator(); it.hasNext();) {
final Unit unit = it.next();
final int cost = UnitAttachment.get((unit).getType()).getTransportCost();
if (capacity >= cost) {
addedLoad.add(transport, cost);
mapping.put(land, transport);
loaded = true;
capacity -= cost;
mapping.put(unit, transport);
it.remove();
}
}
}
return mapping;
}

private static Comparator<Unit> transportsThatPreviouslyUnloadedComeLast() {
return new Comparator<Unit>() {
@Override
public int compare(final Unit t1, final Unit t2) {
if (t1 == t2 || t1.equals(t2)) {
return 0;
}
final boolean t1previous = TransportTracker.hasTransportUnloadedInPreviousPhase(t1);
final boolean t2previous = TransportTracker.hasTransportUnloadedInPreviousPhase(t2);
if (t1previous == t2previous) {
return 0;
}
if (t1previous == false) {
return -1;
}
return 1;
}
};
}

public static List<Unit> findUnitsToLoadOnAirTransports(final Collection<Unit> units,
final Collection<Unit> transports) {
final Collection<Unit> airTransports = Match.getMatches(transports, Matches.UnitIsAirTransport);
Expand Down Expand Up @@ -194,4 +120,84 @@ public int compare(final Unit o1, final Unit o2) {
return totalLoad;
}

public static int getTransportCost(final Collection<Unit> units) {
if (units == null) {
return 0;
}
int cost = 0;
final Iterator<Unit> iter = units.iterator();
while (iter.hasNext()) {
final Unit item = iter.next();
cost += UnitAttachment.get(item.getType()).getTransportCost();
}
return cost;
}

/**
* Returns a map of unit -> transport. Unit must already be loaded in the transport.
*/
private static Map<Unit, Unit> mapTransportsAlreadyLoaded(final Collection<Unit> units,
final Collection<Unit> transports) {
final Collection<Unit> canBeTransported = Match.getMatches(units, Matches.UnitCanBeTransported);
final Collection<Unit> canTransport = Match.getMatches(transports, Matches.UnitCanTransport);
final Map<Unit, Unit> mapping = new HashMap<Unit, Unit>();
final Iterator<Unit> land = canBeTransported.iterator();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The complexity is not trivial in this method. Can you avoid using Iterator? Looks like a foreach or a Java8 stream+filter would both work to simplify things.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually didn't touch this method. Just the mapTransportToLoad* ones. Leaving it alone for now unless I end up needing to update it for other transport enhancements.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok

while (land.hasNext()) {
final Unit currentTransported = land.next();
final Unit transport = TransportTracker.transportedBy(currentTransported);

// already being transported, make sure it is in transports
if (transport == null) {
continue;
}
if (!canTransport.contains(transport)) {
continue;
}
mapping.put(currentTransported, transport);
}
return mapping;
}

private static List<Unit> sortByTransportCapacityAscending(final Collection<Unit> transports) {
final Comparator<Unit> transportCapacityComparator = new Comparator<Unit>() {
@Override
public int compare(final Unit o1, final Unit o2) {
final int capacityLeft1 = TransportTracker.getAvailableCapacity(o1);
final int capacityLeft2 = TransportTracker.getAvailableCapacity(o2);
return capacityLeft1 - capacityLeft2;
}
};
final List<Unit> canTransport = Match.getMatches(transports, Matches.UnitCanTransport);
Collections.sort(canTransport, transportCapacityComparator);
return canTransport;
}

private static List<Unit> sortByTransportCostDescending(final Collection<Unit> units) {
final Comparator<Unit> transportCostComparator = new Comparator<Unit>() {
@Override
public int compare(final Unit o1, final Unit o2) {
final int cost1 = UnitAttachment.get((o1).getUnitType()).getTransportCost();
final int cost2 = UnitAttachment.get((o2).getUnitType()).getTransportCost();
return cost2 - cost1;
}
};
final List<Unit> canBeTransported = Match.getMatches(units, Matches.UnitCanBeTransported);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@RoiEXLab - example of Matching code

Collections.sort(canBeTransported, transportCostComparator);
return canBeTransported;
}

private static Optional<Unit> loadUnitIntoFirstAvailableTransport(final Unit unit, final List<Unit> canTransport,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i like the helper function 🌈

final Map<Unit, Unit> mapping, final IntegerMap<Unit> addedLoad) {
final int cost = UnitAttachment.get((unit).getType()).getTransportCost();
for (final Unit transport : canTransport) {
final int capacity = TransportTracker.getAvailableCapacity(transport) - addedLoad.getInt(transport);
if (capacity >= cost) {
addedLoad.add(transport, cost);
mapping.put(unit, transport);
return Optional.of(transport);
}
}
return Optional.empty();
}

}