package games.strategy.triplea.oddsCalculator.ta;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.FlowLayout;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Image;
import java.awt.Insets;
import java.awt.Window;
import java.awt.event.ActionEvent;
import java.awt.event.MouseEvent;
import java.awt.event.MouseMotionListener;
import java.awt.event.WindowEvent;
import java.text.DecimalFormat;
import java.text.NumberFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.Vector;
import java.util.concurrent.atomic.AtomicReference;
import javax.swing.AbstractAction;
import javax.swing.BorderFactory;
import javax.swing.Box;
import javax.swing.BoxLayout;
import javax.swing.DefaultListCellRenderer;
import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JComboBox;
import javax.swing.JLabel;
import javax.swing.JList;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTextField;
import javax.swing.ListSelectionModel;
import javax.swing.SwingUtilities;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import games.strategy.debug.ClientLogger;
import games.strategy.engine.ClientContext;
import games.strategy.engine.data.GameData;
import games.strategy.engine.data.NamedAttachable;
import games.strategy.engine.data.PlayerID;
import games.strategy.engine.data.ProductionFrontier;
import games.strategy.engine.data.ProductionRule;
import games.strategy.engine.data.Territory;
import games.strategy.engine.data.TerritoryEffect;
import games.strategy.engine.data.Unit;
import games.strategy.engine.data.UnitType;
import games.strategy.engine.framework.ui.background.WaitDialog;
import games.strategy.triplea.Properties;
import games.strategy.triplea.TripleAUnit;
import games.strategy.triplea.attachments.UnitAttachment;
import games.strategy.triplea.delegate.BattleCalculator;
import games.strategy.triplea.delegate.DiceRoll;
import games.strategy.triplea.delegate.Matches;
import games.strategy.triplea.delegate.TerritoryEffectHelper;
import games.strategy.triplea.delegate.UnitBattleComparator;
import games.strategy.triplea.ui.IUIContext;
import games.strategy.triplea.util.UnitCategory;
import games.strategy.triplea.util.UnitSeperator;
import games.strategy.ui.IntTextField;
import games.strategy.ui.ScrollableTextField;
import games.strategy.ui.ScrollableTextFieldListener;
import games.strategy.ui.WidgetChangedListener;
import games.strategy.util.CompositeMatchOr;
import games.strategy.util.IntegerMap;
import games.strategy.util.ListenerList;
import games.strategy.util.Match;
public class OddsCalculatorPanel extends JPanel {
private static final long serialVersionUID = -3559687618320469183L;
private static final String NO_EFFECTS = "*None*";
private final Window m_parent;
private final JLabel m_attackerWin = new JLabel();
private final JLabel m_defenderWin = new JLabel();
private final JLabel m_draw = new JLabel();
private final JLabel m_defenderLeft = new JLabel();
private final JLabel m_attackerLeft = new JLabel();
private final JLabel m_defenderLeftWhenDefenderWon = new JLabel();
private final JLabel m_attackerLeftWhenAttackerWon = new JLabel();
private final JLabel m_averageChangeInTUV = new JLabel();
private final JLabel m_roundsAverage = new JLabel();
private final JLabel m_count = new JLabel();
private final JLabel m_time = new JLabel();
private final IntTextField m_numRuns = new games.strategy.ui.IntTextField();
private final IntTextField m_retreatAfterXRounds = new games.strategy.ui.IntTextField();
private final IntTextField m_retreatAfterXUnitsLeft = new games.strategy.ui.IntTextField();
private final JPanel m_resultsPanel = new JPanel();
private final JButton m_calculateButton = new JButton("Pls Wait, Copying Data...");
private final JButton m_clearButton = new JButton("Clear");
private final JButton m_closeButton = new JButton("Close");
private final JButton m_SwapSidesButton = new JButton("Swap Sides");
private final JButton m_orderOfLossesButton = new JButton("Order Of Losses");
private final JCheckBox m_keepOneAttackingLandUnitCheckBox = new JCheckBox("One attacking land must live");
private final JCheckBox m_amphibiousCheckBox = new JCheckBox("Battle is Amphibious");
private final JCheckBox m_landBattleCheckBox = new JCheckBox("Land Battle");
private final JCheckBox m_retreatWhenOnlyAirLeftCheckBox = new JCheckBox("Retreat when only air left");
private final IUIContext m_context;
private final GameData m_data;
private final IOddsCalculator m_calculator;
private PlayerUnitsPanel m_attackingUnitsPanel;
private PlayerUnitsPanel m_defendingUnitsPanel;
private JComboBox<PlayerID> m_attackerCombo;
private JComboBox<PlayerID> m_defenderCombo;
private JComboBox<PlayerID> m_SwapSidesCombo;
private final JLabel m_attackerUnitsTotalNumber = new JLabel();
private final JLabel m_defenderUnitsTotalNumber = new JLabel();
private final JLabel m_attackerUnitsTotalTUV = new JLabel();
private final JLabel m_defenderUnitsTotalTUV = new JLabel();
private final JLabel m_attackerUnitsTotalHitpoints = new JLabel();
private final JLabel m_defenderUnitsTotalHitpoints = new JLabel();
private final JLabel m_attackerUnitsTotalPower = new JLabel();
private final JLabel m_defenderUnitsTotalPower = new JLabel();
private String m_attackerOrderOfLosses = null;
private String m_defenderOrderOfLosses = null;
private Territory m_location = null;
private JList<String> m_territoryEffectsJList;
private final WidgetChangedListener m_listenerPlayerUnitsPanel = () -> setWidgetActivation();
public OddsCalculatorPanel(final GameData data, final IUIContext context, final Territory location,
final Window parent) {
m_data = data;
m_context = context;
m_location = location;
m_parent = parent;
m_calculateButton.setEnabled(false);
createComponents();
layoutComponents();
setupListeners();
// use the one passed, not the one we found:
if (location != null) {
m_data.acquireReadLock();
try {
m_landBattleCheckBox.setSelected(!location.isWater());
// default to the current player
if (m_data.getSequence().getStep().getPlayerID() != null
&& !m_data.getSequence().getStep().getPlayerID().isNull()) {
m_attackerCombo.setSelectedItem(m_data.getSequence().getStep().getPlayerID());
}
if (!location.isWater()) {
m_defenderCombo.setSelectedItem(location.getOwner());
} else {
// we need to find out the defender for sea zones
for (final PlayerID player : location.getUnits().getPlayersWithUnits()) {
if (player != getAttacker() && !m_data.getRelationshipTracker().isAllied(player, getAttacker())) {
m_defenderCombo.setSelectedItem(player);
break;
}
}
}
updateDefender(location.getUnits().getMatches(Matches.alliedUnit(getDefender(), data)));
updateAttacker(location.getUnits().getMatches(Matches.alliedUnit(getAttacker(), data)));
} finally {
m_data.releaseReadLock();
}
} else {
m_landBattleCheckBox.setSelected(true);
m_defenderCombo.setSelectedItem(data.getPlayerList().getPlayers().iterator().next());
updateDefender(null);
updateAttacker(null);
}
if (OddsCalculatorPanel.percentageOfFreeMemoryAvailable() < 0.4) {
System.gc();
System.runFinalization();
System.gc();
}
m_calculator = new ConcurrentOddsCalculator("BtlCalc Panel");
m_calculator.addOddsCalculatorListener(() -> {
m_calculateButton.setText("Calculate Odds");
m_calculateButton.setEnabled(true);
});
m_calculator.setGameData(m_data);
setWidgetActivation();
revalidate();
}
public void shutdown() {
try {
// use this if not using a static calc, so that we gc the calc and shutdown all threads.
// must be shutdown, as it has a thread pool per each instance.
m_calculator.shutdown();
} catch (final Exception e) {
ClientLogger.logQuietly(e);
}
}
@Override
protected void finalize() throws Throwable {
shutdown();
super.finalize();
}
private static double percentageOfFreeMemoryAvailable() {
final Runtime runtime = Runtime.getRuntime();
final long maxMemory = runtime.maxMemory();
final long memoryAvailable = Math.min(maxMemory, maxMemory - (runtime.totalMemory() - runtime.freeMemory()));
return (((double) memoryAvailable) / ((double) maxMemory));
}
private static long freeMemoryAvailable() {
final Runtime runtime = Runtime.getRuntime();
final long maxMemory = runtime.maxMemory();
return Math.min(maxMemory, maxMemory - (runtime.totalMemory() - runtime.freeMemory()));
}
private PlayerID getDefender() {
return (PlayerID) m_defenderCombo.getSelectedItem();
}
private PlayerID getAttacker() {
return (PlayerID) m_attackerCombo.getSelectedItem();
}
private PlayerID getSwapSides() {
return (PlayerID) m_SwapSidesCombo.getSelectedItem();
}
private void setupListeners() {
m_defenderCombo.addActionListener(e -> {
m_data.acquireReadLock();
try {
if (m_data.getRelationshipTracker().isAllied(getDefender(), getAttacker())) {
m_attackerCombo.setSelectedItem(getEnemy(getDefender()));
}
} finally {
m_data.releaseReadLock();
}
updateDefender(null);
setWidgetActivation();
});
m_attackerCombo.addActionListener(e -> {
m_data.acquireReadLock();
try {
if (m_data.getRelationshipTracker().isAllied(getDefender(), getAttacker())) {
m_defenderCombo.setSelectedItem(getEnemy(getAttacker()));
}
} finally {
m_data.releaseReadLock();
}
updateAttacker(null);
setWidgetActivation();
});
m_amphibiousCheckBox.addActionListener(e -> setWidgetActivation());
m_landBattleCheckBox.addActionListener(e -> {
m_attackerOrderOfLosses = null;
m_defenderOrderOfLosses = null;
updateDefender(null);
updateAttacker(null);
setWidgetActivation();
});
m_calculateButton.addMouseMotionListener(new MouseMotionListener() {
@Override
public void mouseDragged(final MouseEvent e) {}
@Override
public void mouseMoved(final MouseEvent e) {
final String memoryAvailable = "<br/>Percentage of memory available: "
+ String.format("%.2f", (percentageOfFreeMemoryAvailable() * 100)) + "% <br/>Free memory available: "
+ (freeMemoryAvailable() / (1024 * 1024)) + "MB <br/>Maximum allowed memory: "
+ (Runtime.getRuntime().maxMemory() / (1024 * 1024)) + "MB </html>";
if (m_calculateButton.isEnabled()) {
m_calculateButton.setToolTipText("<html>Data copying finished. " + memoryAvailable);
} else {
m_calculateButton.setToolTipText("<html>If this is taking forever to enable, it means "
+ "<br/>you do not have enough memory to copy the data quickly! "
+ "<br/>Consider increasing the max memory for TripleA. " + memoryAvailable);
}
}
});
m_calculateButton.addActionListener(e -> updateStats());
m_closeButton.addActionListener(e -> {
m_attackerOrderOfLosses = null;
m_defenderOrderOfLosses = null;
m_parent.setVisible(false);
shutdown();
m_parent.dispatchEvent(new WindowEvent(m_parent, WindowEvent.WINDOW_CLOSING));
});
m_clearButton.addActionListener(e -> {
m_defendingUnitsPanel.clear();
m_attackingUnitsPanel.clear();
setWidgetActivation();
});
m_SwapSidesButton.addActionListener(e -> {
m_attackerOrderOfLosses = null;
m_defenderOrderOfLosses = null;
List<Unit> getdefenders = new ArrayList<>();
List<Unit> getattackers = new ArrayList<>();
getdefenders = m_defendingUnitsPanel.getUnits();
getattackers = m_attackingUnitsPanel.getUnits();
m_SwapSidesCombo.setSelectedItem(getAttacker());
m_attackerCombo.setSelectedItem(getDefender());
m_defenderCombo.setSelectedItem(getSwapSides());
m_attackingUnitsPanel.init(getAttacker(), getdefenders, isLand());
m_defendingUnitsPanel.init(getDefender(), getattackers, isLand());
setWidgetActivation();
});
m_orderOfLossesButton.addActionListener(e -> {
final OrderOfLossesInputPanel oolPanel = new OrderOfLossesInputPanel(m_attackerOrderOfLosses,
m_defenderOrderOfLosses, m_attackingUnitsPanel.getCategories(), m_defendingUnitsPanel.getCategories(),
m_landBattleCheckBox.isSelected(), m_context, m_data);
if (JOptionPane.OK_OPTION == JOptionPane.showConfirmDialog(OddsCalculatorPanel.this, oolPanel,
"Create Order Of Losses for each side", JOptionPane.OK_CANCEL_OPTION, JOptionPane.PLAIN_MESSAGE)) {
if (OddsCalculator.isValidOrderOfLoss(oolPanel.getAttackerOrder(), m_data)) {
m_attackerOrderOfLosses = oolPanel.getAttackerOrder();
}
if (OddsCalculator.isValidOrderOfLoss(oolPanel.getDefenderOrder(), m_data)) {
m_defenderOrderOfLosses = oolPanel.getDefenderOrder();
}
}
});
if (m_territoryEffectsJList != null) {
m_territoryEffectsJList.addListSelectionListener(e -> setWidgetActivation());
}
m_attackingUnitsPanel.addChangeListener(m_listenerPlayerUnitsPanel);
m_defendingUnitsPanel.addChangeListener(m_listenerPlayerUnitsPanel);
}
private boolean isAmphibiousBattle() {
return (m_landBattleCheckBox.isSelected() && m_amphibiousCheckBox.isSelected());
}
private Collection<TerritoryEffect> getTerritoryEffects() {
final Collection<TerritoryEffect> territoryEffects = new ArrayList<>();
if (m_territoryEffectsJList != null) {
final List<String> selected = m_territoryEffectsJList.getSelectedValuesList();
m_data.acquireReadLock();
try {
final Hashtable<String, TerritoryEffect> allTerritoryEffects = m_data.getTerritoryEffectList();
for (final String selection : selected) {
if (selection.equals(NO_EFFECTS)) {
territoryEffects.clear();
break;
}
territoryEffects.add(allTerritoryEffects.get(selection));
}
} finally {
m_data.releaseReadLock();
}
}
return territoryEffects;
}
private void updateStats() {
if (!SwingUtilities.isEventDispatchThread()) {
throw new IllegalStateException("Wrong thread");
}
final AtomicReference<AggregateResults> results = new AtomicReference<>();
final WaitDialog dialog =
new WaitDialog(this, "Calculating Odds (" + m_calculator.getThreadCount() + " threads)", new AbstractAction() {
private static final long serialVersionUID = -2148507015083214974L;
@Override
public void actionPerformed(final ActionEvent e) {
m_calculator.cancel();
}
});
final AtomicReference<Collection<Unit>> defenders = new AtomicReference<>();
final AtomicReference<Collection<Unit>> attackers = new AtomicReference<>();
dialog.pack();
dialog.setLocationRelativeTo(this);
final Thread calcThread = new Thread(() -> {
try {
// find a territory to fight in
Territory location = null;
if (m_location == null || m_location.isWater() == isLand()) {
for (final Territory t : m_data.getMap()) {
if (t.isWater() == !isLand()) {
location = t;
break;
}
}
} else {
location = m_location;
}
if (location == null) {
throw new IllegalStateException("No territory found that is land:" + isLand());
}
final List<Unit> defending = m_defendingUnitsPanel.getUnits();
final List<Unit> attacking = m_attackingUnitsPanel.getUnits();
List<Unit> bombarding = new ArrayList<>();
if (isLand()) {
bombarding = Match.getMatches(attacking, Matches.unitCanBombard(getAttacker()));
attacking.removeAll(bombarding);
}
m_calculator.setRetreatAfterRound(m_retreatAfterXRounds.getValue());
m_calculator.setRetreatAfterXUnitsLeft(m_retreatAfterXUnitsLeft.getValue());
if (m_retreatWhenOnlyAirLeftCheckBox.isSelected()) {
m_calculator.setRetreatWhenOnlyAirLeft(true);
} else {
m_calculator.setRetreatWhenOnlyAirLeft(false);
}
if (m_landBattleCheckBox.isSelected() && m_keepOneAttackingLandUnitCheckBox.isSelected()) {
m_calculator.setKeepOneAttackingLandUnit(true);
} else {
m_calculator.setKeepOneAttackingLandUnit(false);
}
if (isAmphibiousBattle()) {
m_calculator.setAmphibious(true);
} else {
m_calculator.setAmphibious(false);
}
m_calculator.setAttackerOrderOfLosses(m_attackerOrderOfLosses);
m_calculator.setDefenderOrderOfLosses(m_defenderOrderOfLosses);
final Collection<TerritoryEffect> territoryEffects = getTerritoryEffects();
defenders.set(defending);
attackers.set(attacking);
results.set(m_calculator.setCalculateDataAndCalculate(getAttacker(), getDefender(), location, attacking,
defending, bombarding, territoryEffects, m_numRuns.getValue()));
} finally {
SwingUtilities.invokeLater(() -> {
dialog.setVisible(false);
dialog.dispose();
});
}
}, "Odds calc thread");
// Actually start thread.
calcThread.start();
// the runnable setting the dialog visible must run after this code executes, since this code is running on the
// swing event thread
dialog.setVisible(true);
// results.get() could be null if we cancelled to quickly or something weird like that.
if (results == null || results.get() == null) {
setResultsToBlank();
} else {
m_attackerWin.setText(formatPercentage(results.get().getAttackerWinPercent()));
m_defenderWin.setText(formatPercentage(results.get().getDefenderWinPercent()));
m_draw.setText(formatPercentage(results.get().getDrawPercent()));
final boolean isLand = isLand();
final List<Unit> mainCombatAttackers =
Match.getMatches(attackers.get(), Matches.UnitCanBeInBattle(true, isLand, 1, false, true, true));
final List<Unit> mainCombatDefenders =
Match.getMatches(defenders.get(), Matches.UnitCanBeInBattle(false, isLand, 1, false, true, true));
final int attackersTotal = mainCombatAttackers.size();
final int defendersTotal = mainCombatDefenders.size();
m_defenderLeft.setText(formatValue(results.get().getAverageDefendingUnitsLeft()) + " /" + defendersTotal);
m_attackerLeft.setText(formatValue(results.get().getAverageAttackingUnitsLeft()) + " /" + attackersTotal);
m_defenderLeftWhenDefenderWon
.setText(formatValue(results.get().getAverageDefendingUnitsLeftWhenDefenderWon()) + " /" + defendersTotal);
m_attackerLeftWhenAttackerWon
.setText(formatValue(results.get().getAverageAttackingUnitsLeftWhenAttackerWon()) + " /" + attackersTotal);
m_roundsAverage.setText("" + formatValue(results.get().getAverageBattleRoundsFought()));
try {
m_data.acquireReadLock();
m_averageChangeInTUV.setText("" + formatValue(results.get().getAverageTUVswing(getAttacker(),
mainCombatAttackers, getDefender(), mainCombatDefenders, m_data)));
} finally {
m_data.releaseReadLock();
}
m_count.setText(results.get().getRollCount() + "");
m_time.setText(formatValue(results.get().getTime() / 1000.0) + "s");
}
}
public String formatPercentage(final double percentage) {
final NumberFormat format = new DecimalFormat("%");
return format.format(percentage);
}
public String formatValue(final double value) {
final NumberFormat format = new DecimalFormat("#0.##");
return format.format(value);
}
private void updateDefender(List<Unit> units) {
if (units == null) {
units = Collections.emptyList();
}
final boolean isLand = isLand();
units = Match.getMatches(units, Matches.UnitCanBeInBattle(false, isLand, 1, false, false, false));
m_defendingUnitsPanel.init(getDefender(), units, isLand);
}
private void updateAttacker(List<Unit> units) {
if (units == null) {
units = Collections.emptyList();
}
final boolean isLand = isLand();
units = Match.getMatches(units, Matches.UnitCanBeInBattle(true, isLand, 1, false, false, false));
m_attackingUnitsPanel.init(getAttacker(), units, isLand);
}
private boolean isLand() {
return m_landBattleCheckBox.isSelected();
}
private PlayerID getEnemy(final PlayerID player) {
for (final PlayerID id : m_data.getPlayerList()) {
if (m_data.getRelationshipTracker().isAtWar(player, id)) {
return id;
}
}
for (final PlayerID id : m_data.getPlayerList()) {
if (!m_data.getRelationshipTracker().isAllied(player, id)) {
return id;
}
}
// TODO: do we allow fighting allies in the battle calc?
throw new IllegalStateException("No enemies or non-allies for :" + player);
}
private void layoutComponents() {
setLayout(new BorderLayout());
final JPanel main = new JPanel();
main.setBorder(BorderFactory.createEmptyBorder(10, 0, 10, 0));
add(main, BorderLayout.CENTER);
main.setLayout(new BorderLayout());
final JPanel attackAndDefend = new JPanel();
attackAndDefend.setLayout(new GridBagLayout());
final int gap = 20;
int row0 = 0;
attackAndDefend.add(new JLabel("Attacker: "), new GridBagConstraints(0, row0, 1, 1, 0, 0, GridBagConstraints.EAST,
GridBagConstraints.NONE, new Insets(0, gap, gap, 0), 0, 0));
attackAndDefend.add(m_attackerCombo, new GridBagConstraints(1, row0, 1, 1, 0, 0, GridBagConstraints.EAST,
GridBagConstraints.NONE, new Insets(0, 0, gap / 2, gap), 0, 0));
attackAndDefend.add(new JLabel("Defender: "), new GridBagConstraints(2, row0, 1, 1, 0, 0, GridBagConstraints.EAST,
GridBagConstraints.NONE, new Insets(0, gap, gap, 0), 0, 0));
attackAndDefend.add(m_defenderCombo, new GridBagConstraints(3, row0, 1, 1, 0, 0, GridBagConstraints.EAST,
GridBagConstraints.NONE, new Insets(0, 0, gap / 2, gap), 0, 0));
row0++;
attackAndDefend.add(m_attackerUnitsTotalNumber, new GridBagConstraints(0, row0, 1, 1, 0, 0, GridBagConstraints.EAST,
GridBagConstraints.NONE, new Insets(0, gap, 0, 0), 0, 0));
attackAndDefend.add(m_attackerUnitsTotalTUV, new GridBagConstraints(1, row0, 1, 1, 0, 0, GridBagConstraints.EAST,
GridBagConstraints.NONE, new Insets(0, gap / 2, 0, gap * 2), 0, 0));
attackAndDefend.add(m_defenderUnitsTotalNumber, new GridBagConstraints(2, row0, 1, 1, 0, 0, GridBagConstraints.EAST,
GridBagConstraints.NONE, new Insets(0, gap, 0, 0), 0, 0));
attackAndDefend.add(m_defenderUnitsTotalTUV, new GridBagConstraints(3, row0, 1, 1, 0, 0, GridBagConstraints.EAST,
GridBagConstraints.NONE, new Insets(0, gap / 2, 0, gap * 2), 0, 0));
row0++;
attackAndDefend.add(m_attackerUnitsTotalHitpoints, new GridBagConstraints(0, row0, 1, 1, 0, 0,
GridBagConstraints.EAST, GridBagConstraints.NONE, new Insets(0, gap, gap / 2, 0), 0, 0));
attackAndDefend.add(m_attackerUnitsTotalPower, new GridBagConstraints(1, row0, 1, 1, 0, 0, GridBagConstraints.EAST,
GridBagConstraints.NONE, new Insets(0, gap / 2, gap / 2, gap * 2), 0, 0));
attackAndDefend.add(m_defenderUnitsTotalHitpoints, new GridBagConstraints(2, row0, 1, 1, 0, 0,
GridBagConstraints.EAST, GridBagConstraints.NONE, new Insets(0, gap, gap / 2, 0), 0, 0));
attackAndDefend.add(m_defenderUnitsTotalPower, new GridBagConstraints(3, row0, 1, 1, 0, 0, GridBagConstraints.EAST,
GridBagConstraints.NONE, new Insets(0, gap / 2, gap / 2, gap * 2), 0, 0));
row0++;
final JScrollPane attackerScroll = new JScrollPane(m_attackingUnitsPanel);
attackerScroll.setBorder(null);
attackerScroll.getViewport().setBorder(null);
final JScrollPane defenderScroll = new JScrollPane(m_defendingUnitsPanel);
defenderScroll.setBorder(null);
defenderScroll.getViewport().setBorder(null);
attackAndDefend.add(attackerScroll, new GridBagConstraints(0, row0, 2, 1, 1, 1, GridBagConstraints.NORTH,
GridBagConstraints.BOTH, new Insets(10, gap, gap, gap), 0, 0));
attackAndDefend.add(defenderScroll, new GridBagConstraints(2, row0, 2, 1, 1, 1, GridBagConstraints.NORTH,
GridBagConstraints.BOTH, new Insets(10, gap, gap, gap), 0, 0));
main.add(attackAndDefend, BorderLayout.CENTER);
final JPanel resultsText = new JPanel();
resultsText.setLayout(new GridBagLayout());
int row1 = 0;
resultsText.add(new JLabel("Attacker Wins:"), new GridBagConstraints(0, row1++, 1, 1, 0, 0, GridBagConstraints.EAST,
GridBagConstraints.NONE, new Insets(0, 0, 0, 0), 0, 0));
resultsText.add(new JLabel("Draw:"), new GridBagConstraints(0, row1++, 1, 1, 0, 0, GridBagConstraints.EAST,
GridBagConstraints.NONE, new Insets(0, 0, 0, 0), 0, 0));
resultsText.add(new JLabel("Defender Wins:"), new GridBagConstraints(0, row1++, 1, 1, 0, 0, GridBagConstraints.EAST,
GridBagConstraints.NONE, new Insets(0, 0, 0, 0), 0, 0));
resultsText.add(new JLabel("Ave. Defender Units Left:"), new GridBagConstraints(0, row1++, 1, 1, 0, 0,
GridBagConstraints.EAST, GridBagConstraints.NONE, new Insets(6, 0, 0, 0), 0, 0));
resultsText.add(new JLabel("Units Left If Def Won:"), new GridBagConstraints(0, row1++, 1, 1, 0, 0,
GridBagConstraints.EAST, GridBagConstraints.NONE, new Insets(0, 0, 0, 0), 0, 0));
resultsText.add(new JLabel("Ave. Attacker Units Left:"), new GridBagConstraints(0, row1++, 1, 1, 0, 0,
GridBagConstraints.EAST, GridBagConstraints.NONE, new Insets(6, 0, 0, 0), 0, 0));
resultsText.add(new JLabel("Units Left If Att Won:"), new GridBagConstraints(0, row1++, 1, 1, 0, 0,
GridBagConstraints.EAST, GridBagConstraints.NONE, new Insets(0, 0, 0, 0), 0, 0));
resultsText.add(new JLabel("Average TUV Swing:"), new GridBagConstraints(0, row1++, 1, 1, 0, 0,
GridBagConstraints.EAST, GridBagConstraints.NONE, new Insets(6, 0, 0, 0), 0, 0));
resultsText.add(new JLabel("Average Rounds:"), new GridBagConstraints(0, row1++, 1, 1, 0, 0,
GridBagConstraints.EAST, GridBagConstraints.NONE, new Insets(0, 0, 0, 0), 0, 0));
resultsText.add(new JLabel("Simulation Count:"), new GridBagConstraints(0, row1++, 1, 1, 0, 0,
GridBagConstraints.EAST, GridBagConstraints.NONE, new Insets(15, 0, 0, 0), 0, 0));
resultsText.add(new JLabel("Time:"), new GridBagConstraints(0, row1++, 1, 1, 0, 0, GridBagConstraints.EAST,
GridBagConstraints.NONE, new Insets(0, 0, 0, 0), 0, 0));
resultsText.add(m_calculateButton, new GridBagConstraints(0, row1++, 2, 1, 0, 0, GridBagConstraints.WEST,
GridBagConstraints.BOTH, new Insets(20, 60, 0, 100), 0, 0));
resultsText.add(m_clearButton, new GridBagConstraints(0, row1++, 1, 1, 0, 0, GridBagConstraints.WEST,
GridBagConstraints.BOTH, new Insets(6, 60, 0, 0), 0, 0));
resultsText.add(new JLabel("Run Count:"), new GridBagConstraints(0, row1++, 1, 1, 0, 0, GridBagConstraints.EAST,
GridBagConstraints.NONE, new Insets(20, 0, 0, 0), 0, 0));
resultsText.add(new JLabel("Retreat After Round:"), new GridBagConstraints(0, row1++, 1, 1, 0, 0,
GridBagConstraints.EAST, GridBagConstraints.NONE, new Insets(10, 0, 0, 0), 0, 0));
resultsText.add(new JLabel("Retreat When X Units Left:"), new GridBagConstraints(0, row1++, 1, 1, 0, 0,
GridBagConstraints.EAST, GridBagConstraints.NONE, new Insets(10, 0, 0, 0), 0, 0));
int row2 = 0;
resultsText.add(m_attackerWin, new GridBagConstraints(1, row2++, 1, 1, 0, 0, GridBagConstraints.WEST,
GridBagConstraints.NONE, new Insets(0, 10, 0, 0), 0, 0));
resultsText.add(m_draw, new GridBagConstraints(1, row2++, 1, 1, 0, 0, GridBagConstraints.WEST,
GridBagConstraints.NONE, new Insets(0, 10, 0, 0), 0, 0));
resultsText.add(m_defenderWin, new GridBagConstraints(1, row2++, 1, 1, 0, 0, GridBagConstraints.WEST,
GridBagConstraints.NONE, new Insets(0, 10, 0, 0), 0, 0));
resultsText.add(m_defenderLeft, new GridBagConstraints(1, row2++, 1, 1, 0, 0, GridBagConstraints.WEST,
GridBagConstraints.NONE, new Insets(6, 10, 0, 0), 0, 0));
resultsText.add(m_defenderLeftWhenDefenderWon, new GridBagConstraints(1, row2++, 1, 1, 0, 0,
GridBagConstraints.WEST, GridBagConstraints.NONE, new Insets(0, 10, 0, 0), 0, 0));
resultsText.add(m_attackerLeft, new GridBagConstraints(1, row2++, 1, 1, 0, 0, GridBagConstraints.WEST,
GridBagConstraints.NONE, new Insets(6, 10, 0, 0), 0, 0));
resultsText.add(m_attackerLeftWhenAttackerWon, new GridBagConstraints(1, row2++, 1, 1, 0, 0,
GridBagConstraints.WEST, GridBagConstraints.NONE, new Insets(0, 10, 0, 0), 0, 0));
resultsText.add(m_averageChangeInTUV, new GridBagConstraints(1, row2++, 1, 1, 0, 0, GridBagConstraints.WEST,
GridBagConstraints.NONE, new Insets(6, 10, 0, 0), 0, 0));
resultsText.add(m_roundsAverage, new GridBagConstraints(1, row2++, 1, 1, 0, 0, GridBagConstraints.WEST,
GridBagConstraints.NONE, new Insets(0, 10, 0, 0), 0, 0));
resultsText.add(m_count, new GridBagConstraints(1, row2++, 1, 1, 0, 0, GridBagConstraints.WEST,
GridBagConstraints.NONE, new Insets(15, 10, 0, 0), 0, 0));
resultsText.add(m_time, new GridBagConstraints(1, row2++, 1, 1, 0, 0, GridBagConstraints.WEST,
GridBagConstraints.NONE, new Insets(0, 10, 0, 0), 0, 0));
row2++;
resultsText.add(m_SwapSidesButton, new GridBagConstraints(1, row2++, 1, 1, 0, 0, GridBagConstraints.WEST,
GridBagConstraints.BOTH, new Insets(6, 10, 0, 100), 0, 0));
resultsText.add(m_numRuns, new GridBagConstraints(1, row2++, 1, 1, 0, 0, GridBagConstraints.WEST,
GridBagConstraints.NONE, new Insets(20, 10, 0, 0), 0, 0));
resultsText.add(m_retreatAfterXRounds, new GridBagConstraints(1, row2++, 1, 1, 0, 0, GridBagConstraints.WEST,
GridBagConstraints.NONE, new Insets(10, 10, 0, 0), 0, 0));
resultsText.add(m_retreatAfterXUnitsLeft, new GridBagConstraints(1, row2++, 1, 1, 0, 0, GridBagConstraints.WEST,
GridBagConstraints.NONE, new Insets(10, 10, 0, 0), 0, 0));
row1 = row2;
resultsText.add(m_orderOfLossesButton, new GridBagConstraints(0, row1++, 1, 1, 0, 0, GridBagConstraints.EAST,
GridBagConstraints.BOTH, new Insets(10, 15, 0, 0), 0, 0));
if (m_territoryEffectsJList != null) {
resultsText.add(new JScrollPane(m_territoryEffectsJList),
new GridBagConstraints(0, row1, 1, m_territoryEffectsJList.getVisibleRowCount(), 0, 0,
GridBagConstraints.EAST, GridBagConstraints.BOTH, new Insets(10, 15, 0, 0), 0, 0));
row1 += m_territoryEffectsJList.getVisibleRowCount();
}
resultsText.add(m_retreatWhenOnlyAirLeftCheckBox, new GridBagConstraints(1, row2++, 1, 1, 0, 0,
GridBagConstraints.WEST, GridBagConstraints.NONE, new Insets(10, 10, 0, 5), 0, 0));
resultsText.add(m_keepOneAttackingLandUnitCheckBox, new GridBagConstraints(1, row2++, 1, 1, 0, 0,
GridBagConstraints.WEST, GridBagConstraints.NONE, new Insets(2, 10, 0, 5), 0, 0));
resultsText.add(m_amphibiousCheckBox, new GridBagConstraints(1, row2++, 1, 1, 0, 0, GridBagConstraints.WEST,
GridBagConstraints.NONE, new Insets(2, 10, 0, 5), 0, 0));
resultsText.add(m_landBattleCheckBox, new GridBagConstraints(1, row2++, 1, 1, 0, 0, GridBagConstraints.WEST,
GridBagConstraints.NONE, new Insets(2, 10, 0, 5), 0, 0));
m_resultsPanel.add(resultsText);
m_resultsPanel.setBorder(BorderFactory.createEmptyBorder());
final JScrollPane resultsScroll = new JScrollPane(m_resultsPanel);
resultsScroll.setBorder(BorderFactory.createEmptyBorder());
final Dimension resultsScrollDimensions = resultsScroll.getPreferredSize();
// add some so that we don't have double scroll bars appear when only one is needed
resultsScrollDimensions.width += 22;
resultsScroll.setPreferredSize(resultsScrollDimensions);
main.add(resultsScroll, BorderLayout.EAST);
final JPanel south = new JPanel();
south.setLayout(new BorderLayout());
final JPanel buttons = new JPanel();
buttons.setLayout(new FlowLayout(FlowLayout.CENTER));
buttons.add(m_closeButton);
south.add(buttons, BorderLayout.SOUTH);
add(south, BorderLayout.SOUTH);
}
private void createComponents() {
m_data.acquireReadLock();
try {
final Collection<PlayerID> playerList = new ArrayList<>(m_data.getPlayerList().getPlayers());
if (doesPlayerHaveUnitsOnMap(PlayerID.NULL_PLAYERID, m_data)) {
playerList.add(PlayerID.NULL_PLAYERID);
}
m_attackerCombo = new JComboBox<>(new Vector<>(playerList));
m_defenderCombo = new JComboBox<>(new Vector<>(playerList));
m_SwapSidesCombo = new JComboBox<>(new Vector<>(playerList));
final Hashtable<String, TerritoryEffect> allTerritoryEffects = m_data.getTerritoryEffectList();
if (allTerritoryEffects == null || allTerritoryEffects.isEmpty()) {
m_territoryEffectsJList = null;
} else {
final Vector<String> effectNames = new Vector<>();
effectNames.add(NO_EFFECTS);
effectNames.addAll(allTerritoryEffects.keySet());
m_territoryEffectsJList = new JList<>(effectNames);
m_territoryEffectsJList.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);
m_territoryEffectsJList.setLayoutOrientation(JList.VERTICAL);
// equal to the amount of space left (number of remaining items on the right)
m_territoryEffectsJList.setVisibleRowCount(4);
if (m_location != null) {
final Collection<TerritoryEffect> currentEffects = TerritoryEffectHelper.getEffects(m_location);
if (!currentEffects.isEmpty()) {
final int[] selectedIndexes = new int[currentEffects.size()];
int currentIndex = 0;
for (final TerritoryEffect te : currentEffects) {
selectedIndexes[currentIndex] = effectNames.indexOf(te.getName());
currentIndex++;
}
m_territoryEffectsJList.setSelectedIndices(selectedIndexes);
}
}
}
} finally {
m_data.releaseReadLock();
}
m_defenderCombo.setRenderer(new PlayerRenderer());
m_attackerCombo.setRenderer(new PlayerRenderer());
m_SwapSidesCombo.setRenderer(new PlayerRenderer());
m_defendingUnitsPanel = new PlayerUnitsPanel(m_data, m_context, true);
m_attackingUnitsPanel = new PlayerUnitsPanel(m_data, m_context, false);
m_numRuns.setColumns(4);
m_numRuns.setMin(1);
m_numRuns.setMax(20000);
final int simulationCount =
Properties.getLow_Luck(m_data) ? ClientContext.battleCalcSettings().getSimulationCountLowLuck()
: ClientContext.battleCalcSettings().getSimulationCountDice();
m_numRuns.setValue(simulationCount);
m_retreatAfterXRounds.setColumns(4);
m_retreatAfterXRounds.setMin(-1);
m_retreatAfterXRounds.setMax(1000);
m_retreatAfterXRounds.setValue(-1);
m_retreatAfterXRounds.setToolTipText("-1 means never.");
m_retreatAfterXUnitsLeft.setColumns(4);
m_retreatAfterXUnitsLeft.setMin(-1);
m_retreatAfterXUnitsLeft.setMax(1000);
m_retreatAfterXUnitsLeft.setValue(-1);
m_retreatAfterXUnitsLeft.setToolTipText("-1 means never. If positive and 'retreat when only air left' is also "
+ "selected, then we will retreat when X of non-air units is left.");
setResultsToBlank();
m_defenderLeft.setToolTipText("Units Left does not include AA guns and other infrastructure, and does not include "
+ "Bombarding sea units for land battles.");
m_attackerLeft.setToolTipText("Units Left does not include AA guns and other infrastructure, and does not include "
+ "Bombarding sea units for land battles.");
m_defenderLeftWhenDefenderWon.setToolTipText("Units Left does not include AA guns and other infrastructure, and "
+ "does not include Bombarding sea units for land battles.");
m_attackerLeftWhenAttackerWon.setToolTipText("Units Left does not include AA guns and other infrastructure, and "
+ "does not include Bombarding sea units for land battles.");
m_averageChangeInTUV.setToolTipText("TUV Swing does not include captured AA guns and other infrastructure, and "
+ "does not include Bombarding sea units for land battles.");
m_retreatWhenOnlyAirLeftCheckBox.setToolTipText("We retreat if only air is left, and if 'retreat when x units "
+ "left' is positive we will retreat when x of non-air is left too.");
m_attackerUnitsTotalNumber.setToolTipText("Totals do not include AA guns and other infrastructure, and does not "
+ "include Bombarding sea units for land battles.");
m_defenderUnitsTotalNumber.setToolTipText("Totals do not include AA guns and other infrastructure, and does not "
+ "include Bombarding sea units for land battles.");
}
private void setResultsToBlank() {
final String blank = "------";
m_attackerWin.setText(blank);
m_defenderWin.setText(blank);
m_draw.setText(blank);
m_defenderLeft.setText(blank);
m_attackerLeft.setText(blank);
m_defenderLeftWhenDefenderWon.setText(blank);
m_attackerLeftWhenAttackerWon.setText(blank);
m_roundsAverage.setText(blank);
m_averageChangeInTUV.setText(blank);
m_count.setText(blank);
m_time.setText(blank);
}
public void setWidgetActivation() {
m_keepOneAttackingLandUnitCheckBox.setEnabled(m_landBattleCheckBox.isSelected());
m_amphibiousCheckBox.setEnabled(m_landBattleCheckBox.isSelected());
final boolean isLand = isLand();
try {
m_data.acquireReadLock();
// do not include bombardment and aa guns in our "total" labels
final List<Unit> attackers = Match.getMatches(m_attackingUnitsPanel.getUnits(),
Matches.UnitCanBeInBattle(true, isLand, 1, false, true, true));
final List<Unit> defenders = Match.getMatches(m_defendingUnitsPanel.getUnits(),
Matches.UnitCanBeInBattle(false, isLand, 1, false, true, true));
m_attackerUnitsTotalNumber.setText("Units: " + attackers.size());
m_defenderUnitsTotalNumber.setText("Units: " + defenders.size());
m_attackerUnitsTotalTUV.setText("TUV: " + BattleCalculator.getTUV(attackers, getAttacker(),
BattleCalculator.getCostsForTUV(getAttacker(), m_data), m_data));
m_defenderUnitsTotalTUV.setText("TUV: " + BattleCalculator.getTUV(defenders, getDefender(),
BattleCalculator.getCostsForTUV(getDefender(), m_data), m_data));
final int attackHP = BattleCalculator.getTotalHitpointsLeft(attackers);
final int defenseHP = BattleCalculator.getTotalHitpointsLeft(defenders);
m_attackerUnitsTotalHitpoints.setText("HP: " + attackHP);
m_defenderUnitsTotalHitpoints.setText("HP: " + defenseHP);
final boolean isAmphibiousBattle = isAmphibiousBattle();
final Collection<TerritoryEffect> territoryEffects = getTerritoryEffects();
final IntegerMap<UnitType> costs = BattleCalculator.getCostsForTUV(getAttacker(), m_data);
Collections.sort(attackers, new UnitBattleComparator(false, costs, territoryEffects, m_data, false, false));
Collections.reverse(attackers);
final int attackPower = DiceRoll.getTotalPower(DiceRoll.getUnitPowerAndRollsForNormalBattles(attackers, defenders,
false, false, m_data, m_location, territoryEffects, isAmphibiousBattle,
(isAmphibiousBattle ? attackers : new ArrayList<>())), m_data);
// defender is never amphibious
final int defensePower =
DiceRoll
.getTotalPower(
DiceRoll.getUnitPowerAndRollsForNormalBattles(defenders, attackers, true, false,
m_data, m_location, territoryEffects, isAmphibiousBattle, new ArrayList<>()),
m_data);
m_attackerUnitsTotalPower.setText("Power: " + attackPower);
m_defenderUnitsTotalPower.setText("Power: " + defensePower);
} finally {
m_data.releaseReadLock();
}
}
class PlayerRenderer extends DefaultListCellRenderer {
private static final long serialVersionUID = -7639128794342607309L;
@Override
public Component getListCellRendererComponent(final JList<?> list, final Object value, final int index,
final boolean isSelected, final boolean cellHasFocus) {
super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
final PlayerID id = (PlayerID) value;
setText(id.getName());
setIcon(new ImageIcon(m_context.getFlagImageFactory().getSmallFlag(id)));
return this;
}
}
public void selectCalculateButton() {
m_calculateButton.requestFocus();
}
private static boolean doesPlayerHaveUnitsOnMap(final PlayerID player, final GameData data) {
for (final Territory t : data.getMap()) {
for (final Unit u : t.getUnits()) {
if (u.getOwner().equals(player)) {
return true;
}
}
}
return false;
}
}
class PlayerUnitsPanel extends JPanel {
private static final long serialVersionUID = -1206338960403314681L;
private final GameData m_data;
private final IUIContext m_context;
private final boolean m_defender;
private boolean m_isLand = true;
private List<UnitCategory> m_categories = null;
private final ListenerList<WidgetChangedListener> m_listeners = new ListenerList<>();
private final WidgetChangedListener m_listenerUnitPanel = () -> notifyListeners();
PlayerUnitsPanel(final GameData data, final IUIContext context, final boolean defender) {
m_data = data;
m_context = context;
m_defender = defender;
setLayout(new BoxLayout(this, BoxLayout.Y_AXIS));
}
public void clear() {
for (final Component c : getComponents()) {
final UnitPanel panel = (UnitPanel) c;
panel.setCount(0);
}
}
public List<Unit> getUnits() {
final List<Unit> allUnits = new ArrayList<>();
for (final Component c : getComponents()) {
final UnitPanel panel = (UnitPanel) c;
allUnits.addAll(panel.getUnits());
}
return allUnits;
}
public List<UnitCategory> getCategories() {
return m_categories;
}
public void init(final PlayerID id, final List<Unit> units, final boolean land) {
m_isLand = land;
m_categories = new ArrayList<>(categorize(id, units));
Collections.sort(m_categories, (o1, o2) -> {
final UnitType ut1 = o1.getType();
final UnitType ut2 = o2.getType();
final UnitAttachment u1 = UnitAttachment.get(ut1);
final UnitAttachment u2 = UnitAttachment.get(ut2);
// for land, we want land, air, aa gun, then bombarding
if (land) {
if (u1.getIsSea() != u2.getIsSea()) {
return u1.getIsSea() ? 1 : -1;
}
if (Matches.UnitTypeIsAAforAnything.match(ut1) != Matches.UnitTypeIsAAforAnything.match(ut2)) {
return Matches.UnitTypeIsAAforAnything.match(ut1) ? 1 : -1;
}
if (u1.getIsAir() != u2.getIsAir()) {
return u1.getIsAir() ? 1 : -1;
}
} else {
if (u1.getIsSea() != u2.getIsSea()) {
return u1.getIsSea() ? -1 : 1;
}
}
return u1.getName().compareTo(u2.getName());
});
removeAll();
Match<UnitType> predicate;
if (land) {
if (m_defender) {
predicate = Matches.UnitTypeIsNotSea;
} else {
predicate = new CompositeMatchOr<>(Matches.UnitTypeIsNotSea, Matches.unitTypeCanBombard(id));
}
} else {
predicate = Matches.UnitTypeIsSeaOrAir;
}
final IntegerMap<UnitType> costs;
try {
m_data.acquireReadLock();
costs = BattleCalculator.getCostsForTUV(id, m_data);
} finally {
m_data.releaseReadLock();
}
for (final UnitCategory category : m_categories) {
if (predicate.match(category.getType())) {
final UnitPanel upanel = new UnitPanel(m_data, m_context, category, costs);
upanel.addChangeListener(m_listenerUnitPanel);
add(upanel);
}
}
invalidate();
validate();
revalidate();
getParent().invalidate();
}
private Set<UnitCategory> categorize(final PlayerID id, final List<Unit> units) {
// these are the units that exist
final Set<UnitCategory> categories = UnitSeperator.categorize(units);
// the units that can be produced or moved in
for (final UnitType t : getUnitTypes(id)) {
final UnitCategory category = new UnitCategory(t, id);
categories.add(category);
}
return categories;
}
/**
* return all the unit types available for the given player. a unit type is
* available if the unit is producable, or if a player has one
*/
private Collection<UnitType> getUnitTypes(final PlayerID player) {
Collection<UnitType> rVal = new HashSet<>();
final ProductionFrontier frontier = player.getProductionFrontier();
if (frontier != null) {
for (final ProductionRule rule : frontier) {
for (final NamedAttachable type : rule.getResults().keySet()) {
if (type instanceof UnitType) {
rVal.add((UnitType) type);
}
}
}
}
for (final Territory t : m_data.getMap()) {
for (final Unit u : t.getUnits()) {
if (u.getOwner().equals(player)) {
rVal.add(u.getType());
}
}
}
// we want to filter out anything like factories, or units that have no combat ability AND cannot be taken
// casualty.
// in addition, as of right now AA guns cannot fire on the offensive side, so we want to take them out too, unless
// they have other
// combat abilities.
rVal = Match.getMatches(rVal,
Matches.UnitTypeCanBeInBattle(!m_defender, m_isLand, player, 1, false, false, false));
return rVal;
}
public void addChangeListener(final WidgetChangedListener listener) {
m_listeners.add(listener);
}
public void removeChangeListener(final WidgetChangedListener listener) {
m_listeners.remove(listener);
}
private void notifyListeners() {
for (final WidgetChangedListener listener : m_listeners) {
listener.widgetChanged();
}
}
}
class UnitPanel extends JPanel {
private static final long serialVersionUID = 1509643150038705671L;
private final IUIContext m_context;
private final UnitCategory m_category;
private final ScrollableTextField m_textField;
private final GameData m_data;
private final ListenerList<WidgetChangedListener> m_listeners = new ListenerList<>();
private final ScrollableTextFieldListener m_listenerTextField = field -> notifyListeners();
public UnitPanel(final GameData data, final IUIContext context, final UnitCategory category,
final IntegerMap<UnitType> costs) {
m_category = category;
m_context = context;
m_data = data;
m_textField = new ScrollableTextField(0, 512);
m_textField.setShowMaxAndMin(false);
m_textField.addChangeListener(m_listenerTextField);
final String toolTipText = "<html>" + m_category.getType().getName() + ": " + costs.getInt(m_category.getType())
+ " cost, <br /> " + m_category.getType().getTooltip(m_category.getOwner())
+ "</html>";
setCount(m_category.getUnits().size());
setLayout(new GridBagLayout());
final Optional<Image> img =
m_context.getUnitImageFactory().getImage(m_category.getType(), m_category.getOwner(), m_data,
m_category.hasDamageOrBombingUnitDamage(), m_category.getDisabled());
final JLabel label = img.isPresent() ? new JLabel(new ImageIcon(img.get())) : new JLabel();
label.setToolTipText(toolTipText);
add(label, new GridBagConstraints(0, 0, 1, 1, 0, 0, GridBagConstraints.EAST, GridBagConstraints.NONE,
new Insets(0, 0, 0, 10), 0, 0));
add(m_textField, new GridBagConstraints(1, 0, 1, 1, 0, 0, GridBagConstraints.EAST, GridBagConstraints.NONE,
new Insets(0, 0, 0, 0), 0, 0));
}
public List<Unit> getUnits() {
final List<Unit> units = m_category.getType().create(m_textField.getValue(), m_category.getOwner(), true);
if (!units.isEmpty()) {
// creating the unit just makes it, we want to make sure it is damaged if the category says it is damaged
if (m_category.getHitPoints() > 1 && m_category.getDamaged() > 0) {
// we do not need to use bridge and change factory here because this is not sent over the network. these are
// just some temporary
// units for the battle calc.
for (final Unit u : units) {
u.setHits(m_category.getDamaged());
}
}
if (m_category.getDisabled() && Matches.UnitTypeCanBeDamaged.match(m_category.getType())) {
// add 1 because it is the max operational damage and we want to disable it
final int uDamage = Math.max(0, 1 + UnitAttachment.get(m_category.getType()).getMaxOperationalDamage());
for (final Unit u : units) {
((TripleAUnit) u).setUnitDamage(uDamage);
}
}
}
return units;
}
public int getCount() {
return m_textField.getValue();
}
public void setCount(final int value) {
m_textField.setValue(value);
}
public UnitCategory getCategory() {
return m_category;
}
public void addChangeListener(final WidgetChangedListener listener) {
m_listeners.add(listener);
}
public void removeChangeListener(final WidgetChangedListener listener) {
m_listeners.remove(listener);
}
private void notifyListeners() {
for (final WidgetChangedListener listener : m_listeners) {
listener.widgetChanged();
}
}
}
class OrderOfLossesInputPanel extends JPanel {
private static final long serialVersionUID = 8815617685388156219L;
private final GameData m_data;
private final IUIContext m_context;
private final List<UnitCategory> m_attackerCategories;
private final List<UnitCategory> m_defenderCategories;
private final JTextField m_attackerTextField;
private final JTextField m_defenderTextField;
private final JLabel m_attackerLabel = new JLabel("Attacker Units:");
private final JLabel m_defenderLabel = new JLabel("Defender Units:");
private final JButton m_clear;
private final boolean m_land;
public OrderOfLossesInputPanel(final String attackerOrder, final String defenderOrder,
final List<UnitCategory> attackerCategories, final List<UnitCategory> defenderCategories, final boolean land,
final IUIContext context, final GameData data) {
m_data = data;
m_context = context;
m_land = land;
m_attackerCategories = attackerCategories;
m_defenderCategories = defenderCategories;
m_attackerTextField = new JTextField(attackerOrder == null ? "" : attackerOrder);
m_attackerTextField.getDocument().addDocumentListener(new DocumentListener() {
@Override
public void insertUpdate(final DocumentEvent e) {
if (!OddsCalculator.isValidOrderOfLoss(m_attackerTextField.getText(), m_data)) {
m_attackerLabel.setForeground(Color.red);
} else {
m_attackerLabel.setForeground(null);
}
}
@Override
public void removeUpdate(final DocumentEvent e) {
if (!OddsCalculator.isValidOrderOfLoss(m_attackerTextField.getText(), m_data)) {
m_attackerLabel.setForeground(Color.red);
} else {
m_attackerLabel.setForeground(null);
}
}
@Override
public void changedUpdate(final DocumentEvent e) {
if (!OddsCalculator.isValidOrderOfLoss(m_attackerTextField.getText(), m_data)) {
m_attackerLabel.setForeground(Color.red);
} else {
m_attackerLabel.setForeground(null);
}
}
});
m_defenderTextField = new JTextField(defenderOrder == null ? "" : defenderOrder);
m_defenderTextField.getDocument().addDocumentListener(new DocumentListener() {
@Override
public void insertUpdate(final DocumentEvent e) {
if (!OddsCalculator.isValidOrderOfLoss(m_defenderTextField.getText(), m_data)) {
m_defenderLabel.setForeground(Color.red);
} else {
m_defenderLabel.setForeground(null);
}
}
@Override
public void removeUpdate(final DocumentEvent e) {
if (!OddsCalculator.isValidOrderOfLoss(m_defenderTextField.getText(), m_data)) {
m_defenderLabel.setForeground(Color.red);
} else {
m_defenderLabel.setForeground(null);
}
}
@Override
public void changedUpdate(final DocumentEvent e) {
if (!OddsCalculator.isValidOrderOfLoss(m_defenderTextField.getText(), m_data)) {
m_defenderLabel.setForeground(Color.red);
} else {
m_defenderLabel.setForeground(null);
}
}
});
m_clear = new JButton("Clear");
m_clear.addActionListener(e -> {
m_attackerTextField.setText("");
m_defenderTextField.setText("");
});
layoutComponents();
}
private void layoutComponents() {
this.setLayout(new BoxLayout(this, BoxLayout.Y_AXIS));
final JLabel instructions = new JLabel("<html>Here you can specify the 'Order of Losses' (OOL) for each side."
+ "<br />Damageable units will be damanged first always. If the player label is red, your OOL is invalid."
+ "<br />The engine will take your input and add all units to a list starting on the RIGHT side of your text "
+ "line."
+ "<br />Then, during combat, casualties will be chosen starting on the LEFT side of your OOL." + "<br />"
+ OddsCalculator.OOL_SEPARATOR + " separates unit types." + "<br />" + OddsCalculator.OOL_AMOUNT_DESCRIPTOR
+ " is in front of the unit type and describes the number of units." + "<br />" + OddsCalculator.OOL_ALL
+ " means all units of that type." + "<br />Examples:" + "<br />" + OddsCalculator.OOL_ALL
+ OddsCalculator.OOL_AMOUNT_DESCRIPTOR + "infantry" + OddsCalculator.OOL_SEPARATOR + OddsCalculator.OOL_ALL
+ OddsCalculator.OOL_AMOUNT_DESCRIPTOR + "artillery" + OddsCalculator.OOL_SEPARATOR + OddsCalculator.OOL_ALL
+ OddsCalculator.OOL_AMOUNT_DESCRIPTOR + "fighter"
+ "<br />The above will take all infantry, then all artillery, then all fighters, then all other units as "
+ "casualty."
+ "<br /><br />1" + OddsCalculator.OOL_AMOUNT_DESCRIPTOR + "infantry" + OddsCalculator.OOL_SEPARATOR + "2"
+ OddsCalculator.OOL_AMOUNT_DESCRIPTOR + "artillery" + OddsCalculator.OOL_SEPARATOR + "6"
+ OddsCalculator.OOL_AMOUNT_DESCRIPTOR + "fighter"
+ "<br />The above will take 1 infantry, then 2 artillery, then 6 fighters, then all other units as casualty."
+ "<br /><br />" + OddsCalculator.OOL_ALL + OddsCalculator.OOL_AMOUNT_DESCRIPTOR + "infantry"
+ OddsCalculator.OOL_SEPARATOR + OddsCalculator.OOL_ALL + OddsCalculator.OOL_AMOUNT_DESCRIPTOR + "fighter"
+ OddsCalculator.OOL_SEPARATOR + "1" + OddsCalculator.OOL_AMOUNT_DESCRIPTOR + "infantry"
+ "<br />The above will take all except 1 infantry casualty, then all fighters, then the last infantry, then "
+ "all other units casualty.</html>");
instructions.setAlignmentX(Component.CENTER_ALIGNMENT);
this.add(instructions);
this.add(Box.createVerticalStrut(30));
m_attackerLabel.setAlignmentX(Component.CENTER_ALIGNMENT);
this.add(m_attackerLabel);
final JPanel attackerUnits = getUnitButtonPanel(m_attackerCategories, m_attackerTextField);
attackerUnits.setAlignmentX(Component.CENTER_ALIGNMENT);
this.add(attackerUnits);
m_attackerTextField.setAlignmentX(Component.CENTER_ALIGNMENT);
this.add(m_attackerTextField);
this.add(Box.createVerticalStrut(30));
m_defenderLabel.setAlignmentX(Component.CENTER_ALIGNMENT);
this.add(m_defenderLabel);
final JPanel defenderUnits = getUnitButtonPanel(m_defenderCategories, m_defenderTextField);
defenderUnits.setAlignmentX(Component.CENTER_ALIGNMENT);
this.add(defenderUnits);
m_defenderTextField.setAlignmentX(Component.CENTER_ALIGNMENT);
this.add(m_defenderTextField);
this.add(Box.createVerticalStrut(10));
m_clear.setAlignmentX(Component.CENTER_ALIGNMENT);
this.add(m_clear);
}
private JPanel getUnitButtonPanel(final List<UnitCategory> categories, final JTextField textField) {
final JPanel panel = new JPanel();
panel.setLayout(new BoxLayout(panel, BoxLayout.X_AXIS));
if (categories != null) {
final Set<UnitType> typesUsed = new HashSet<>();
for (final UnitCategory category : categories) {
// no duplicates or infrastructure allowed. no sea if land, no land if sea.
if (typesUsed.contains(category.getType()) || Matches.UnitTypeIsInfrastructure.match(category.getType())
|| (m_land && Matches.UnitTypeIsSea.match(category.getType()))
|| (!m_land && Matches.UnitTypeIsLand.match(category.getType()))) {
continue;
}
final String unitName =
OddsCalculator.OOL_ALL + OddsCalculator.OOL_AMOUNT_DESCRIPTOR + category.getType().getName();
final String toolTipText = "<html>" + category.getType().getName() + ": "
+ category.getType().getTooltip(category.getOwner()) + "</html>";
final Optional<Image> img =
m_context.getUnitImageFactory().getImage(category.getType(), category.getOwner(), m_data,
category.hasDamageOrBombingUnitDamage(), category.getDisabled());
if (img.isPresent()) {
final JButton button = new JButton(new ImageIcon(img.get()));
button.setToolTipText(toolTipText);
button.addActionListener(e -> textField
.setText((textField.getText().length() > 0 ? (textField.getText() + OddsCalculator.OOL_SEPARATOR) : "")
+ unitName));
panel.add(button);
}
typesUsed.add(category.getType());
}
}
return panel;
}
public String getAttackerOrder() {
return m_attackerTextField.getText();
}
public String getDefenderOrder() {
return m_defenderTextField.getText();
}
}