44import mage .cards .Card ;
55import mage .cards .decks .Deck ;
66import mage .client .util .GUISizeHelper ;
7+ import org .apache .log4j .Logger ;
78
89import java .awt .*;
10+ import java .io .BufferedReader ;
11+ import java .io .InputStream ;
12+ import java .io .InputStreamReader ;
913import java .util .List ;
1014import java .util .*;
1115import java .util .stream .Stream ;
1620 * <p>
1721 * Support:
1822 * - [x] game changers
19- * - [ ] infinite combos
23+ * - [x ] infinite combos
2024 * - [x] mass land destruction
2125 * - [x] extra turns
2226 * - [x] tutors
27+ * Features:
28+ * - [x] find possible bracket level of the deck
29+ * - [x] find affected cards by checking group
30+ * - [x] can auto-generate infinite combos list, see verify test downloadAndPrepareCommanderBracketsData
31+ * - [ ] TODO: tests
32+ * - [ ] TODO: table - players brackets level disclose settings
33+ * - [ ] TODO: generate - convert card name to xmage format and assert on bad names (ascii only)
2334 *
2435 * @author JayDi85
2536 */
2637public class BracketLegalityLabel extends LegalityLabel {
2738
39+ private static final Logger logger = Logger .getLogger (BracketLegalityLabel .class );
40+
2841 private static final String GROUP_GAME_CHANGES = "Game Changers" ;
29- private static final String GROUP_INFINITE_COMBOS = "Infinite Combos (unsupported) " ;
42+ private static final String GROUP_INFINITE_COMBOS = "Infinite Combos" ;
3043 private static final String GROUP_MASS_LAND_DESTRUCTION = "Mass Land Destruction" ;
3144 private static final String GROUP_EXTRA_TURN = "Extra Turns" ;
3245 private static final String GROUP_TUTORS = "Tutors" ;
3346
34- private final BracketLevel level ;
47+ private static final Map <String , List <Integer >> MAX_GROUP_LIMITS = new LinkedHashMap <>();
48+
49+ static {
50+ // 1
51+ // No cards from the Game Changer list.
52+ // No intentional two-card infinite combos.
53+ // No mass land destruction.
54+ // No extra turn cards.
55+ // Tutors should be sparse.
56+ // 2
57+ // No cards from the Game Changer list.
58+ // No intentional two-card infinite combos.
59+ // No mass land destruction.
60+ // Extra turn cards should only appear in low quantities and should not be chained in succession or looped.
61+ // Tutors should be sparse.
62+ // 3
63+ // Up to three (3) cards from the Game Changer list.
64+ // No intentional early game two-card infinite combos.
65+ // No mass land destruction.
66+ // Extra turn cards should only appear in low quantities and should not be chained in succession or looped.
67+ // 4
68+ // 5
69+ // allow any cards
70+
71+ // cards limits per brackets level, it's ok to use 99 as max
72+ // group - levels 0, 1, 2, 3, 4, 5
73+ MAX_GROUP_LIMITS .put (GROUP_GAME_CHANGES ,
74+ Arrays .asList (0 , 0 , 0 , 3 , 99 , 99 ));
75+ MAX_GROUP_LIMITS .put (GROUP_INFINITE_COMBOS ,
76+ Arrays .asList (0 , 0 , 0 , 0 , 99 , 99 ));
77+ MAX_GROUP_LIMITS .put (GROUP_MASS_LAND_DESTRUCTION ,
78+ Arrays .asList (0 , 0 , 0 , 0 , 99 , 99 ));
79+ MAX_GROUP_LIMITS .put (GROUP_EXTRA_TURN ,
80+ Arrays .asList (0 , 0 , 0 , 3 , 99 , 99 ));
81+ MAX_GROUP_LIMITS .put (GROUP_TUTORS ,
82+ Arrays .asList (0 , 3 , 3 , 99 , 99 , 99 ));
83+ }
84+
85+ private static final String RESOURCE_INFINITE_COMBOS = "brackets/infinite-combos.txt" ;
86+
87+ private final String fullName ;
88+ private final String shortName ;
89+ private final int maxLevel ;
3590
3691 private final List <String > foundGameChangers = new ArrayList <>();
3792 private final List <String > foundInfiniteCombos = new ArrayList <>();
@@ -41,28 +96,14 @@ public class BracketLegalityLabel extends LegalityLabel {
4196
4297 private final List <String > badCards = new ArrayList <>();
4398 private final List <String > fullGameChanges = new ArrayList <>();
99+ private final Set <String > fullInfiniteCombos = new HashSet <>(); // card1@card2, sorted by names, name must be xmage compatible
44100
45- public enum BracketLevel {
46- BRACKET_1 ("Bracket 1" ),
47- BRACKET_2_3 ("Bracket 2-3" ),
48- BRACKET_4_5 ("Bracket 4-5" );
49-
50- private final String name ;
51-
52- BracketLevel (String name ) {
53- this .name = name ;
54- }
55-
56- @ Override
57- public String toString () {
58- return this .name ;
59- }
60- }
61-
62- public BracketLegalityLabel (BracketLevel level ) {
63- super (level .toString (), null );
64- this .level = level ;
65- setPreferredSize (DIM_PREFERRED );
101+ public BracketLegalityLabel (String fullName , String shortName , int maxLevel ) {
102+ super (shortName , null );
103+ this .fullName = fullName ;
104+ this .shortName = shortName ;
105+ this .maxLevel = maxLevel ;
106+ setPreferredSize (DIM_PREFERRED_1_OF_5 );
66107 }
67108
68109 @ Override
@@ -72,81 +113,68 @@ public List<String> selectCards() {
72113
73114 private void validateBracketLevel () {
74115 this .badCards .clear ();
75- switch (this .level ) {
76- case BRACKET_1 :
77- // No cards from the Game Changer list.
78- // No intentional two-card infinite combos.
79- // No mass land destruction.
80- // No extra turn cards.
81- // Tutors should be sparse.
82- this .badCards .addAll (this .foundGameChangers );
83- this .badCards .addAll (this .foundInfiniteCombos );
84- this .badCards .addAll (this .foundMassLandDestruction );
85- this .badCards .addAll (this .foundExtraTurn );
86- if (this .foundTutors .size () > 3 ) {
87- this .badCards .addAll (this .foundTutors );
88- }
89- break ;
90- case BRACKET_2_3 :
91- // 2
92- // No cards from the Game Changer list.
93- // No intentional two-card infinite combos.
94- // No mass land destruction.
95- // Extra turn cards should only appear in low quantities and should not be chained in succession or looped.
96- // Tutors should be sparse.
97- // 3
98- // Up to three (3) cards from the Game Changer list.
99- // No intentional early game two-card infinite combos.
100- // No mass land destruction.
101- // Extra turn cards should only appear in low quantities and should not be chained in succession or looped.
102- if (this .foundGameChangers .size () > 3 ) {
103- this .badCards .addAll (this .foundGameChangers );
104- }
105- this .badCards .addAll (this .foundInfiniteCombos );
106- this .badCards .addAll (this .foundMassLandDestruction );
107- if (this .foundExtraTurn .size () > 3 ) {
108- this .badCards .addAll (this .foundExtraTurn );
109- }
110- // this.badCards.addAll(this.foundTutors); // allow any amount
111- break ;
112- case BRACKET_4_5 :
113- // allow any cards
114- break ;
115- default :
116- throw new IllegalArgumentException ("Unsupported level: " + this .level );
116+
117+ if (this .foundGameChangers .size () > getMaxCardsLimit (GROUP_GAME_CHANGES )) {
118+ this .badCards .addAll (this .foundGameChangers );
119+ }
120+ if (this .foundInfiniteCombos .size () > getMaxCardsLimit (GROUP_INFINITE_COMBOS )) {
121+ this .badCards .addAll (this .foundInfiniteCombos );
122+ }
123+ if (this .foundMassLandDestruction .size () > getMaxCardsLimit (GROUP_MASS_LAND_DESTRUCTION )) {
124+ this .badCards .addAll (this .foundMassLandDestruction );
125+ }
126+ if (this .foundExtraTurn .size () > getMaxCardsLimit (GROUP_EXTRA_TURN )) {
127+ this .badCards .addAll (this .foundExtraTurn );
128+ }
129+ if (this .foundTutors .size () > getMaxCardsLimit (GROUP_TUTORS )) {
130+ this .badCards .addAll (this .foundTutors );
117131 }
118132 }
119133
134+ private Integer getMaxCardsLimit (String groupName ) {
135+ return MAX_GROUP_LIMITS .get (groupName ).get (this .maxLevel );
136+ }
137+
120138 @ Override
121139 public void validateDeck (Deck deck ) {
122140 collectAll (deck );
123141 validateBracketLevel ();
124142
125- int infoFontSize = Math .round (GUISizeHelper .cardTooltipFont .getSize () * 0.6f );
143+ int infoFontHeaderSize = Math .round (GUISizeHelper .cardTooltipFont .getSize () * 1.0f );
144+ int infoFontTextSize = Math .round (GUISizeHelper .cardTooltipFont .getSize () * 0.6f );
126145
127146 // show all found cards in any use cases
128147 Color showColor = this .badCards .isEmpty () ? COLOR_LEGAL : COLOR_NOT_LEGAL ;
129148
130149 List <String > showInfo = new ArrayList <>();
131150 if (this .badCards .isEmpty ()) {
132- showInfo .add ("<p>Deck is <span style='color:green;font-weight:bold;'>GOOD</span> for " + this .level + "</p>" );
151+ 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>" ,
152+ infoFontHeaderSize ,
153+ this .fullName
154+ ));
133155 } else {
134- showInfo .add ("<p>Deck is <span style='color:#BF544A;font-weight:bold;'>BAD</span> for " + this .level + "</p>" );
156+ 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>" ,
157+ infoFontHeaderSize ,
158+ this .fullName
159+ ));
135160 showInfo .add ("<p>(click here to select all bad cards)</p>" );
136161 }
137162
138163 Map <String , List <String >> groups = new LinkedHashMap <>();
139- groups .put (GROUP_GAME_CHANGES , this .foundGameChangers );
140- groups .put (GROUP_INFINITE_COMBOS , this .foundInfiniteCombos );
141- groups .put (GROUP_MASS_LAND_DESTRUCTION , this .foundMassLandDestruction );
142- groups .put (GROUP_EXTRA_TURN , this .foundExtraTurn );
143- groups .put (GROUP_TUTORS , this .foundTutors );
164+ groups .put (GROUP_GAME_CHANGES + getStats ( GROUP_GAME_CHANGES ) , this .foundGameChangers );
165+ groups .put (GROUP_INFINITE_COMBOS + getStats ( GROUP_INFINITE_COMBOS ) , this .foundInfiniteCombos );
166+ groups .put (GROUP_MASS_LAND_DESTRUCTION + getStats ( GROUP_MASS_LAND_DESTRUCTION ) , this .foundMassLandDestruction );
167+ groups .put (GROUP_EXTRA_TURN + getStats ( GROUP_EXTRA_TURN ) , this .foundExtraTurn );
168+ groups .put (GROUP_TUTORS + getStats ( GROUP_TUTORS ) , this .foundTutors );
144169 groups .forEach ((group , cards ) -> {
145170 showInfo .add ("<br>" );
146- showInfo .add ("<br>" );
147- showInfo .add ("<span style='font-weight:bold;'>" + group + ": " + cards .size () + "</span>" );
148- if (!cards .isEmpty ()) {
149- showInfo .add ("<ul style=\" font-size: " + infoFontSize + "px; width: " + TOOLTIP_TABLE_WIDTH + "px; padding-left: 10px; margin: 0;\" >" );
171+ showInfo .add ("<span style='font-weight:bold;font-size: " + infoFontTextSize + "px;'>" + group + "</span>" );
172+ if (cards .isEmpty ()) {
173+ showInfo .add ("<ul style=\" font-size: " + infoFontTextSize + "px; width: " + TOOLTIP_TABLE_WIDTH + "px; padding-left: 10px; margin: 0;\" >" );
174+ showInfo .add ("<li style=\" margin-bottom: 2px;\" >no cards</li>" );
175+ showInfo .add ("</ul>" );
176+ } else {
177+ showInfo .add ("<ul style=\" font-size: " + infoFontTextSize + "px; width: " + TOOLTIP_TABLE_WIDTH + "px; padding-left: 10px; margin: 0;\" >" );
150178 cards .forEach (s -> showInfo .add (String .format ("<li style=\" margin-bottom: 2px;\" >%s</li>" , s )));
151179 showInfo .add ("</ul>" );
152180 }
@@ -156,6 +184,39 @@ public void validateDeck(Deck deck) {
156184 showState (showColor , showText , false );
157185 }
158186
187+ private String getStats (String groupName ) {
188+ int currentAmount = 0 ;
189+ switch (groupName ) {
190+ case GROUP_GAME_CHANGES :
191+ currentAmount = this .foundGameChangers .size ();
192+ break ;
193+ case GROUP_INFINITE_COMBOS :
194+ currentAmount = this .foundInfiniteCombos .size ();
195+ break ;
196+ case GROUP_MASS_LAND_DESTRUCTION :
197+ currentAmount = this .foundMassLandDestruction .size ();
198+ break ;
199+ case GROUP_EXTRA_TURN :
200+ currentAmount = this .foundExtraTurn .size ();
201+ break ;
202+ case GROUP_TUTORS :
203+ currentAmount = this .foundTutors .size ();
204+ break ;
205+ default :
206+ throw new IllegalArgumentException ("Unknown group " + groupName );
207+ }
208+ int maxAmount = MAX_GROUP_LIMITS .get (groupName ).get (this .maxLevel );
209+
210+ String info ;
211+ if (currentAmount > maxAmount ) {
212+ info = " (<span style='color:#BF544A;'>%s of %s</span>)" ;
213+ } else {
214+ info = " (<span>%s of %s</span>)" ;
215+ }
216+
217+ return String .format (info , currentAmount , maxAmount == 99 ? "any" : maxAmount );
218+ }
219+
159220 private void collectAll (Deck deck ) {
160221 collectGameChangers (deck );
161222 collectInfiniteCombos (deck );
@@ -243,8 +304,64 @@ private void collectGameChangers(Deck deck) {
243304 }
244305
245306 private void collectInfiniteCombos (Deck deck ) {
246- // TODO: implement
247307 this .foundInfiniteCombos .clear ();
308+
309+ if (this .fullInfiniteCombos .isEmpty ()) {
310+ InputStream in = BracketLegalityLabel .class .getClassLoader ().getResourceAsStream (RESOURCE_INFINITE_COMBOS );
311+ if (in == null ) {
312+ throw new RuntimeException ("Commander brackets: can't load infinite combos list" );
313+ }
314+ try (InputStreamReader input = new InputStreamReader (in );
315+ BufferedReader reader = new BufferedReader (input )) {
316+ String line = reader .readLine ();
317+ while (line != null ) {
318+ try {
319+ line = line .trim ();
320+ if (line .startsWith ("#" )) {
321+ continue ;
322+ }
323+ List <String > cards = Arrays .asList (line .split ("@" ));
324+ if (cards .size () != 2 ) {
325+ logger .warn ("wrong line format in commander brackets file: " + line );
326+ continue ;
327+ }
328+
329+ Collections .sort (cards );
330+ this .fullInfiniteCombos .add (String .join ("@" , cards ));
331+ } finally {
332+ line = reader .readLine ();
333+ }
334+ }
335+ } catch (Exception e ) {
336+ throw new RuntimeException ("Tokens brackets: can't load infinite combos list - " + e );
337+ }
338+ }
339+
340+ // search and check all x2 combinations
341+ List <Card > deckCards = new ArrayList <>();
342+ Set <Card > foundCards = new HashSet <>();
343+ deckCards .addAll (deck .getCards ());
344+ deckCards .addAll (deck .getSideboard ());
345+ for (Card card1 : deckCards ) {
346+ for (Card card2 : deckCards ) {
347+ if (card1 == card2 ) {
348+ continue ;
349+ }
350+ List <String > names = Arrays .asList (card1 .getName (), card2 .getName ());
351+ Collections .sort (names );
352+ String deckCombo = String .join ("@" , names );
353+ if (this .fullInfiniteCombos .contains (deckCombo )) {
354+ foundCards .add (card1 );
355+ foundCards .add (card2 );
356+ break ;
357+ }
358+ }
359+ }
360+
361+ foundCards .stream ()
362+ .map (MageObject ::getName )
363+ .sorted ()
364+ .forEach (this .foundInfiniteCombos ::add );
248365 }
249366
250367 private void collectMassLandDestruction (Deck deck ) {
0 commit comments