GUI, deck: improved bracket level calculations (added all levels to validator panel, improved stats and visual, part of #13341)

This commit is contained in:
Oleg Agafonov 2025-06-06 23:06:49 +04:00
parent f511b85e3c
commit d5dc85a54c
4 changed files with 134 additions and 88 deletions

View file

@ -30,7 +30,6 @@ import java.util.stream.Stream;
* - [x] can auto-generate infinite combos list, see verify test downloadAndPrepareCommanderBracketsData * - [x] can auto-generate infinite combos list, see verify test downloadAndPrepareCommanderBracketsData
* - [ ] TODO: tests * - [ ] TODO: tests
* - [ ] TODO: table - players brackets level disclose settings * - [ ] TODO: table - players brackets level disclose settings
* - [ ] TODO: deck - improve gui to show more levels
* - [ ] TODO: generate - convert card name to xmage format and assert on bad names (ascii only) * - [ ] TODO: generate - convert card name to xmage format and assert on bad names (ascii only)
* *
* @author JayDi85 * @author JayDi85
@ -45,66 +44,15 @@ public class BracketLegalityLabel extends LegalityLabel {
private static final String GROUP_EXTRA_TURN = "Extra Turns"; private static final String GROUP_EXTRA_TURN = "Extra Turns";
private static final String GROUP_TUTORS = "Tutors"; private static final String GROUP_TUTORS = "Tutors";
private static final String RESOURCE_INFINITE_COMBOS = "brackets/infinite-combos.txt"; private static final Map<String, List<Integer>> MAX_GROUP_LIMITS = new LinkedHashMap<>();
private final BracketLevel level; static {
// 1
private final List<String> foundGameChangers = new ArrayList<>();
private final List<String> foundInfiniteCombos = new ArrayList<>();
private final List<String> foundMassLandDestruction = new ArrayList<>();
private final List<String> foundExtraTurn = new ArrayList<>();
private final List<String> foundTutors = new ArrayList<>();
private final List<String> badCards = new ArrayList<>();
private final List<String> fullGameChanges = new ArrayList<>();
private final Set<String> fullInfiniteCombos = new HashSet<>(); // card1@card2, sorted by names, name must be xmage compatible
public enum BracketLevel {
BRACKET_1("Bracket 1"),
BRACKET_2_3("Bracket 2-3"),
BRACKET_4_5("Bracket 4-5");
private final String name;
BracketLevel(String name) {
this.name = name;
}
@Override
public String toString() {
return this.name;
}
}
public BracketLegalityLabel(BracketLevel level) {
super(level.toString(), null);
this.level = level;
setPreferredSize(DIM_PREFERRED);
}
@Override
public List<String> selectCards() {
return new ArrayList<>(this.badCards);
}
private void validateBracketLevel() {
this.badCards.clear();
switch (this.level) {
case BRACKET_1:
// No cards from the Game Changer list. // No cards from the Game Changer list.
// No intentional two-card infinite combos. // No intentional two-card infinite combos.
// No mass land destruction. // No mass land destruction.
// No extra turn cards. // No extra turn cards.
// Tutors should be sparse. // Tutors should be sparse.
this.badCards.addAll(this.foundGameChangers);
this.badCards.addAll(this.foundInfiniteCombos);
this.badCards.addAll(this.foundMassLandDestruction);
this.badCards.addAll(this.foundExtraTurn);
if (this.foundTutors.size() > 3) {
this.badCards.addAll(this.foundTutors);
}
break;
case BRACKET_2_3:
// 2 // 2
// No cards from the Game Changer list. // No cards from the Game Changer list.
// No intentional two-card infinite combos. // No intentional two-card infinite combos.
@ -116,54 +64,117 @@ public class BracketLegalityLabel extends LegalityLabel {
// No intentional early game two-card infinite combos. // No intentional early game two-card infinite combos.
// No mass land destruction. // No mass land destruction.
// Extra turn cards should only appear in low quantities and should not be chained in succession or looped. // Extra turn cards should only appear in low quantities and should not be chained in succession or looped.
if (this.foundGameChangers.size() > 3) { // 4
// 5
// allow any cards
// cards limits per brackets level, it's ok to use 99 as max
// group - levels 0, 1, 2, 3, 4, 5
MAX_GROUP_LIMITS.put(GROUP_GAME_CHANGES,
Arrays.asList(0, 0, 0, 3, 99, 99));
MAX_GROUP_LIMITS.put(GROUP_INFINITE_COMBOS,
Arrays.asList(0, 0, 0, 0, 99, 99));
MAX_GROUP_LIMITS.put(GROUP_MASS_LAND_DESTRUCTION,
Arrays.asList(0, 0, 0, 0, 99, 99));
MAX_GROUP_LIMITS.put(GROUP_EXTRA_TURN,
Arrays.asList(0, 0, 0, 3, 99, 99));
MAX_GROUP_LIMITS.put(GROUP_TUTORS,
Arrays.asList(0, 3, 3, 99, 99, 99));
}
private static final String RESOURCE_INFINITE_COMBOS = "brackets/infinite-combos.txt";
private final String fullName;
private final String shortName;
private final int maxLevel;
private final List<String> foundGameChangers = new ArrayList<>();
private final List<String> foundInfiniteCombos = new ArrayList<>();
private final List<String> foundMassLandDestruction = new ArrayList<>();
private final List<String> foundExtraTurn = new ArrayList<>();
private final List<String> foundTutors = new ArrayList<>();
private final List<String> badCards = new ArrayList<>();
private final List<String> fullGameChanges = new ArrayList<>();
private final Set<String> fullInfiniteCombos = new HashSet<>(); // card1@card2, sorted by names, name must be xmage compatible
public BracketLegalityLabel(String fullName, String shortName, int maxLevel) {
super(shortName, null);
this.fullName = fullName;
this.shortName = shortName;
this.maxLevel = maxLevel;
setPreferredSize(DIM_PREFERRED_1_OF_5);
}
@Override
public List<String> selectCards() {
return new ArrayList<>(this.badCards);
}
private void validateBracketLevel() {
this.badCards.clear();
if (this.foundGameChangers.size() > getMaxCardsLimit(GROUP_GAME_CHANGES)) {
this.badCards.addAll(this.foundGameChangers); this.badCards.addAll(this.foundGameChangers);
} }
if (this.foundInfiniteCombos.size() > getMaxCardsLimit(GROUP_INFINITE_COMBOS)) {
this.badCards.addAll(this.foundInfiniteCombos); this.badCards.addAll(this.foundInfiniteCombos);
}
if (this.foundMassLandDestruction.size() > getMaxCardsLimit(GROUP_MASS_LAND_DESTRUCTION)) {
this.badCards.addAll(this.foundMassLandDestruction); this.badCards.addAll(this.foundMassLandDestruction);
if (this.foundExtraTurn.size() > 3) { }
if (this.foundExtraTurn.size() > getMaxCardsLimit(GROUP_EXTRA_TURN)) {
this.badCards.addAll(this.foundExtraTurn); this.badCards.addAll(this.foundExtraTurn);
} }
// this.badCards.addAll(this.foundTutors); // allow any amount if (this.foundTutors.size() > getMaxCardsLimit(GROUP_TUTORS)) {
break; this.badCards.addAll(this.foundTutors);
case BRACKET_4_5:
// allow any cards
break;
default:
throw new IllegalArgumentException("Unsupported level: " + this.level);
} }
} }
private Integer getMaxCardsLimit(String groupName) {
return MAX_GROUP_LIMITS.get(groupName).get(this.maxLevel);
}
@Override @Override
public void validateDeck(Deck deck) { public void validateDeck(Deck deck) {
collectAll(deck); collectAll(deck);
validateBracketLevel(); validateBracketLevel();
int infoFontSize = Math.round(GUISizeHelper.cardTooltipFont.getSize() * 0.6f); int infoFontHeaderSize = Math.round(GUISizeHelper.cardTooltipFont.getSize() * 1.0f);
int infoFontTextSize = Math.round(GUISizeHelper.cardTooltipFont.getSize() * 0.6f);
// show all found cards in any use cases // show all found cards in any use cases
Color showColor = this.badCards.isEmpty() ? COLOR_LEGAL : COLOR_NOT_LEGAL; Color showColor = this.badCards.isEmpty() ? COLOR_LEGAL : COLOR_NOT_LEGAL;
List<String> showInfo = new ArrayList<>(); List<String> showInfo = new ArrayList<>();
if (this.badCards.isEmpty()) { if (this.badCards.isEmpty()) {
showInfo.add("<p>Deck is <span style='color:green;font-weight:bold;'>GOOD</span> for " + this.level + "</p>"); showInfo.add(String.format("<span style='font-weight:bold;font-size:%dpx;'><p>Deck is <span style='color:green;'>GOOD</span> for %s</p></span>",
infoFontHeaderSize,
this.fullName
));
} else { } else {
showInfo.add("<p>Deck is <span style='color:#BF544A;font-weight:bold;'>BAD</span> for " + this.level + "</p>"); showInfo.add(String.format("<span style='font-weight:bold;font-size:%dpx;'><p>Deck is <span style='color:#BF544A;'>BAD</span> for %s</p></span>",
infoFontHeaderSize,
this.fullName
));
showInfo.add("<p>(click here to select all bad cards)</p>"); showInfo.add("<p>(click here to select all bad cards)</p>");
} }
Map<String, List<String>> groups = new LinkedHashMap<>(); Map<String, List<String>> groups = new LinkedHashMap<>();
groups.put(GROUP_GAME_CHANGES, this.foundGameChangers); groups.put(GROUP_GAME_CHANGES + getStats(GROUP_GAME_CHANGES), this.foundGameChangers);
groups.put(GROUP_INFINITE_COMBOS, this.foundInfiniteCombos); groups.put(GROUP_INFINITE_COMBOS + getStats(GROUP_INFINITE_COMBOS), this.foundInfiniteCombos);
groups.put(GROUP_MASS_LAND_DESTRUCTION, this.foundMassLandDestruction); groups.put(GROUP_MASS_LAND_DESTRUCTION + getStats(GROUP_MASS_LAND_DESTRUCTION), this.foundMassLandDestruction);
groups.put(GROUP_EXTRA_TURN, this.foundExtraTurn); groups.put(GROUP_EXTRA_TURN + getStats(GROUP_EXTRA_TURN), this.foundExtraTurn);
groups.put(GROUP_TUTORS, this.foundTutors); groups.put(GROUP_TUTORS + getStats(GROUP_TUTORS), this.foundTutors);
groups.forEach((group, cards) -> { groups.forEach((group, cards) -> {
showInfo.add("<br>"); showInfo.add("<br>");
showInfo.add("<br>"); showInfo.add("<span style='font-weight:bold;font-size: " + infoFontTextSize + "px;'>" + group + "</span>");
showInfo.add("<span style='font-weight:bold;'>" + group + ": " + cards.size() + "</span>"); if (cards.isEmpty()) {
if (!cards.isEmpty()) { showInfo.add("<ul style=\"font-size: " + infoFontTextSize + "px; width: " + TOOLTIP_TABLE_WIDTH + "px; padding-left: 10px; margin: 0;\">");
showInfo.add("<ul style=\"font-size: " + infoFontSize + "px; width: " + TOOLTIP_TABLE_WIDTH + "px; padding-left: 10px; margin: 0;\">"); showInfo.add("<li style=\"margin-bottom: 2px;\">no cards</li>");
showInfo.add("</ul>");
} else {
showInfo.add("<ul style=\"font-size: " + infoFontTextSize + "px; width: " + TOOLTIP_TABLE_WIDTH + "px; padding-left: 10px; margin: 0;\">");
cards.forEach(s -> showInfo.add(String.format("<li style=\"margin-bottom: 2px;\">%s</li>", s))); cards.forEach(s -> showInfo.add(String.format("<li style=\"margin-bottom: 2px;\">%s</li>", s)));
showInfo.add("</ul>"); showInfo.add("</ul>");
} }
@ -173,6 +184,39 @@ public class BracketLegalityLabel extends LegalityLabel {
showState(showColor, showText, false); showState(showColor, showText, false);
} }
private String getStats(String groupName) {
int currentAmount = 0;
switch (groupName) {
case GROUP_GAME_CHANGES:
currentAmount = this.foundGameChangers.size();
break;
case GROUP_INFINITE_COMBOS:
currentAmount = this.foundInfiniteCombos.size();
break;
case GROUP_MASS_LAND_DESTRUCTION:
currentAmount = this.foundMassLandDestruction.size();
break;
case GROUP_EXTRA_TURN:
currentAmount = this.foundExtraTurn.size();
break;
case GROUP_TUTORS:
currentAmount = this.foundTutors.size();
break;
default:
throw new IllegalArgumentException("Unknown group " + groupName);
}
int maxAmount = MAX_GROUP_LIMITS.get(groupName).get(this.maxLevel);
String info;
if (currentAmount > maxAmount) {
info = " (<span style='color:#BF544A;'>%s of %s</span>)";
} else {
info = " (<span>%s of %s</span>)";
}
return String.format(info, currentAmount, maxAmount == 99 ? "any" : maxAmount);
}
private void collectAll(Deck deck) { private void collectAll(Deck deck) {
collectGameChangers(deck); collectGameChangers(deck);
collectInfiniteCombos(deck); collectInfiniteCombos(deck);

View file

@ -2,7 +2,6 @@ package mage.client.components;
import mage.cards.decks.Deck; import mage.cards.decks.Deck;
import mage.client.util.GUISizeHelper; import mage.client.util.GUISizeHelper;
import mage.client.util.gui.GuiDisplayUtil;
import mage.deck.Commander; import mage.deck.Commander;
import java.util.ArrayList; import java.util.ArrayList;
@ -22,7 +21,7 @@ public class EdhPowerLevelLegalityLabel extends LegalityLabel {
public EdhPowerLevelLegalityLabel() { public EdhPowerLevelLegalityLabel() {
super("EDH Power Level: ?", null); super("EDH Power Level: ?", null);
setPreferredSize(DIM_PREFERRED_X3); setPreferredSize(DIM_PREFERRED_3_OF_3);
} }
@Override @Override

View file

@ -25,9 +25,10 @@ public class LegalityLabel extends JLabel {
protected static final Color COLOR_TEXT = new Color(255, 255, 255); protected static final Color COLOR_TEXT = new Color(255, 255, 255);
protected static final Dimension DIM_MINIMUM = new Dimension(75, 25); protected static final Dimension DIM_MINIMUM = new Dimension(75, 25);
protected static final Dimension DIM_MAXIMUM = new Dimension(150, 75); protected static final Dimension DIM_MAXIMUM = new Dimension(150, 75);
protected static final Dimension DIM_PREFERRED = new Dimension(75, 25); protected static final Dimension DIM_PREFERRED_1_OF_3 = new Dimension(75, 25);
protected static final Dimension DIM_PREFERRED_X2 = new Dimension(DIM_PREFERRED.width * 2 + 5, 25); protected static final Dimension DIM_PREFERRED_2_OF_3 = new Dimension(DIM_PREFERRED_1_OF_3.width * 2 + 5, 25);
protected static final Dimension DIM_PREFERRED_X3 = new Dimension(DIM_PREFERRED.width * 3 + 5 + 5, 25); protected static final Dimension DIM_PREFERRED_3_OF_3 = new Dimension(DIM_PREFERRED_1_OF_3.width * 3 + 5 * 2, 25);
protected static final Dimension DIM_PREFERRED_1_OF_5 = new Dimension((DIM_PREFERRED_3_OF_3.width - 5 * 4) / 5, 25);
protected static final int TOOLTIP_TABLE_WIDTH = 400; // size of the label's tooltip protected static final int TOOLTIP_TABLE_WIDTH = 400; // size of the label's tooltip
protected static final int TOOLTIP_MAX_ERRORS = 20; // max errors to show in tooltip protected static final int TOOLTIP_MAX_ERRORS = 20; // max errors to show in tooltip
@ -54,7 +55,7 @@ public class LegalityLabel extends JLabel {
setMaximumSize(DIM_MAXIMUM); setMaximumSize(DIM_MAXIMUM);
setName(text); // NOI18N setName(text); // NOI18N
setOpaque(true); setOpaque(true);
setPreferredSize(DIM_PREFERRED); setPreferredSize(DIM_PREFERRED_1_OF_3);
} }
/** /**
@ -80,7 +81,7 @@ public class LegalityLabel extends JLabel {
setMinimumSize(DIM_MINIMUM); setMinimumSize(DIM_MINIMUM);
setMaximumSize(DIM_MAXIMUM); setMaximumSize(DIM_MAXIMUM);
setOpaque(true); setOpaque(true);
setPreferredSize(DIM_PREFERRED); setPreferredSize(DIM_PREFERRED_1_OF_3);
} }
/** /**
@ -91,7 +92,7 @@ public class LegalityLabel extends JLabel {
button.setHorizontalAlignment(SwingConstants.CENTER); button.setHorizontalAlignment(SwingConstants.CENTER);
button.setMinimumSize(DIM_MINIMUM); button.setMinimumSize(DIM_MINIMUM);
button.setMaximumSize(DIM_MAXIMUM); button.setMaximumSize(DIM_MAXIMUM);
button.setPreferredSize(DIM_PREFERRED); button.setPreferredSize(DIM_PREFERRED_1_OF_3);
return button; return button;
} }

View file

@ -107,9 +107,11 @@ public class DeckLegalityPanel extends javax.swing.JPanel {
// extra buttons like score // extra buttons like score
this.add(new EdhPowerLevelLegalityLabel()); this.add(new EdhPowerLevelLegalityLabel());
// only 3 buttons allowed for one line // only 3 buttons allowed for one line
this.add(new BracketLegalityLabel(BracketLegalityLabel.BracketLevel.BRACKET_1)); this.add(new BracketLegalityLabel("Bracket 1", "B1", 1));
this.add(new BracketLegalityLabel(BracketLegalityLabel.BracketLevel.BRACKET_2_3)); this.add(new BracketLegalityLabel("Bracket 2", "B2", 2));
this.add(new BracketLegalityLabel(BracketLegalityLabel.BracketLevel.BRACKET_4_5)); this.add(new BracketLegalityLabel("Bracket 3", "B3", 3));
this.add(new BracketLegalityLabel("Bracket 4", "B4", 4));
this.add(new BracketLegalityLabel("Bracket 5", "B5", 5));
addHidePanelButton(); addHidePanelButton();