From 4063ac55e68c2054d493431773e9328fdf36cd7d Mon Sep 17 00:00:00 2001 From: Hans Mackowiak Date: Thu, 23 Oct 2025 23:04:22 +0200 Subject: [PATCH] AbilityUtils: refactor Party types (#8967) --- .../src/main/java/forge/card/CardType.java | 15 ++- .../main/java/forge/card/CardTypeView.java | 1 + .../src/main/java/forge/game/ForgeScript.java | 2 + .../java/forge/game/ability/AbilityUtils.java | 94 ++++++++++--------- .../ability/effects/ChooseCardEffect.java | 3 +- .../java/forge/game/card/CardProperty.java | 12 --- 6 files changed, 68 insertions(+), 59 deletions(-) diff --git a/forge-core/src/main/java/forge/card/CardType.java b/forge-core/src/main/java/forge/card/CardType.java index b8998629be2..5a92fe00436 100644 --- a/forge-core/src/main/java/forge/card/CardType.java +++ b/forge-core/src/main/java/forge/card/CardType.java @@ -547,7 +547,14 @@ public final class CardType implements Comparable, CardTypeView { if (!isCreature() && !isKindred()) { return false; } - return !Collections.disjoint(getCreatureTypes(), Constant.OUTLAW_TYPES); + return Constant.OUTLAW_TYPES.stream().anyMatch(s -> hasCreatureType(s)); + } + @Override + public boolean isParty() { + if (!isCreature() && !isKindred()) { + return false; + } + return Constant.PARTY_TYPES.stream().anyMatch(s -> hasCreatureType(s)); } @Override @@ -916,6 +923,12 @@ public final class CardType implements Comparable, CardTypeView { "Pirate", "Rogue", "Warlock"); + + public static final Set PARTY_TYPES = Sets.newHashSet( + "Cleric", + "Rogue", + "Warrior", + "Wizard"); } public static class Predicates { public static Predicate IS_LAND_TYPE = CardType::isALandType; diff --git a/forge-core/src/main/java/forge/card/CardTypeView.java b/forge-core/src/main/java/forge/card/CardTypeView.java index 4667c48704b..048e9d4cb74 100644 --- a/forge-core/src/main/java/forge/card/CardTypeView.java +++ b/forge-core/src/main/java/forge/card/CardTypeView.java @@ -64,6 +64,7 @@ public interface CardTypeView extends Iterable, Serializable { boolean isSaga(); boolean isHistoric(); boolean isOutlaw(); + boolean isParty(); CardTypeView getTypeWithChanges(Iterable changedCardTypes); } diff --git a/forge-game/src/main/java/forge/game/ForgeScript.java b/forge-game/src/main/java/forge/game/ForgeScript.java index cf7a461f92b..4215d0bc1fd 100644 --- a/forge-game/src/main/java/forge/game/ForgeScript.java +++ b/forge-game/src/main/java/forge/game/ForgeScript.java @@ -93,6 +93,8 @@ public class ForgeScript { } } else if (property.equals("Outlaw")) { return type.isOutlaw(); + } else if (property.equals("Party")) { + return type.isParty(); } else if (property.startsWith("non")) { // ... Other Card types return !type.hasStringType(property.substring(3)); diff --git a/forge-game/src/main/java/forge/game/ability/AbilityUtils.java b/forge-game/src/main/java/forge/game/ability/AbilityUtils.java index 2e07716ef58..da85d2910c6 100644 --- a/forge-game/src/main/java/forge/game/ability/AbilityUtils.java +++ b/forge-game/src/main/java/forge/game/ability/AbilityUtils.java @@ -4,6 +4,7 @@ import com.google.common.collect.*; import com.google.common.math.IntMath; import forge.card.CardStateName; import forge.card.CardType; +import forge.card.CardTypeView; import forge.card.ColorSet; import forge.card.MagicColor; import forge.card.mana.ManaAtom; @@ -42,6 +43,7 @@ import java.util.*; import java.util.Map.Entry; import java.util.regex.Matcher; import java.util.regex.Pattern; +import java.util.stream.Collectors; public class AbilityUtils { private final static ImmutableList cmpList = ImmutableList.of("LT", "LE", "EQ", "GE", "GT", "NE"); @@ -2601,61 +2603,65 @@ public class AbilityUtils { } if (sq[0].contains("Party")) { - CardCollection adventurers = CardLists.getValidCards(player.getCardsIn(ZoneType.Battlefield), - "Creature.Cleric,Creature.Rogue,Creature.Warrior,Creature.Wizard", player, c, ctb); - - Set partyTypes = Sets.newHashSet("Cleric", "Rogue", "Warrior", "Wizard"); - int partySize = 0; - - HashMap chosenParty = new HashMap<>(); - List wildcard = Lists.newArrayList(); - HashMap> multityped = new HashMap<>(); + Set chosenParty = Sets.newHashSet(); + int wildcard = 0; + ListMultimap multityped = MultimapBuilder.hashKeys().arrayListValues().build(); + List chosenMulti = Lists.newArrayList(); // Figure out how to count each class separately. - for (Card card : adventurers) { - // cards with all creature types will just return full list - Set creatureTypes = card.getType().getCreatureTypes(); - creatureTypes.retainAll(partyTypes); - - if (creatureTypes.size() == 4) { - wildcard.add(card); - - if (wildcard.size() >= 4) { - break; - } + for (Card card : player.getCardsIn(ZoneType.Battlefield)) { + if (!card.isCreature()) { continue; - } else if (creatureTypes.size() == 1) { - String type = (String)(creatureTypes.toArray()[0]); + } + CardTypeView type = card.getType(); + Set creatureTypes; - if (!chosenParty.containsKey(type)) { - chosenParty.put(type, card); + // extra logic for "all creature types" cards + if (type.hasAllCreatureTypes()) { + // one of the party types could be excluded, so check each of them separate + creatureTypes = CardType.Constant.PARTY_TYPES.stream().filter(p -> type.hasCreatureType(p)).collect(Collectors.toSet()); + } else { // shortcut for others + creatureTypes = type.getCreatureTypes(); + creatureTypes.retainAll(CardType.Constant.PARTY_TYPES); + } + + switch (creatureTypes.size()) { + case 0: + continue; + case 4: + wildcard++; + break; + case 1: + chosenParty.addAll(creatureTypes); + break; + default: + for (String t : creatureTypes) { + multityped.put(t, card); } - } else { - multityped.put(card, creatureTypes); + } + + // found enough + if (chosenParty.size() + wildcard >= 4) { + break; } } - partySize = Math.min(chosenParty.size() + wildcard.size(), 4); + if (chosenParty.size() + wildcard < 4) { + multityped.keySet().removeAll(chosenParty); - if (partySize < 4) { - partyTypes.removeAll(chosenParty.keySet()); - - // Here I'm left with just the party types that I haven't selected. - for (Card multi : multityped.keySet()) { - Set types = multityped.get(multi); - types.retainAll(partyTypes); - - for (String type : types) { - chosenParty.put(type, multi); - partyTypes.remove(type); - break; - } - } + // sort by amount of members + Multimaps.asMap(multityped).entrySet().stream() + .sorted(Map.Entry.>comparingByValue(Comparator.>comparingInt(Collection::size))) + .forEach(e -> { + e.getValue().removeAll(chosenMulti); + if (e.getValue().size() > 0) { + chosenParty.add(e.getKey()); + chosenMulti.add(e.getValue().get(0)); + } + }); } - partySize = Math.min(chosenParty.size() + wildcard.size(), 4); - - return doXMath(partySize, expr, c, ctb); + return doXMath(Math.min(chosenParty.size() + wildcard, 4), expr, c, ctb); } // TODO make AI part to understand Sunburst better so this isn't needed diff --git a/forge-game/src/main/java/forge/game/ability/effects/ChooseCardEffect.java b/forge-game/src/main/java/forge/game/ability/effects/ChooseCardEffect.java index 19ee339ae2b..6c66183eb8c 100644 --- a/forge-game/src/main/java/forge/game/ability/effects/ChooseCardEffect.java +++ b/forge-game/src/main/java/forge/game/ability/effects/ChooseCardEffect.java @@ -131,8 +131,7 @@ public class ChooseCardEffect extends SpellAbilityEffect { } } else if (sa.hasParam("ChooseEach")) { final String s = sa.getParam("ChooseEach"); - final String[] types = s.equals("Party") ? new String[]{"Cleric","Rogue","Warrior","Wizard"} - : s.split(" & "); + final Collection types = s.equals("Party") ? CardType.Constant.PARTY_TYPES : Arrays.asList(s.split(" & ")); for (final String type : types) { CardCollection valids = CardLists.filter(pChoices, CardPredicates.isType(type)); if (!valids.isEmpty()) { diff --git a/forge-game/src/main/java/forge/game/card/CardProperty.java b/forge-game/src/main/java/forge/game/card/CardProperty.java index e27a4f8fed7..0be78b8237f 100644 --- a/forge-game/src/main/java/forge/game/card/CardProperty.java +++ b/forge-game/src/main/java/forge/game/card/CardProperty.java @@ -2,7 +2,6 @@ package forge.game.card; import com.google.common.collect.Iterables; import com.google.common.collect.Lists; -import com.google.common.collect.Sets; import forge.StaticData; import forge.card.CardDb; import forge.card.ColorSet; @@ -720,17 +719,6 @@ public class CardProperty { return false; } } - } else if (property.equals("Party")) { - boolean isParty = false; - Set partyTypes = Sets.newHashSet("Cleric", "Rogue", "Warrior", "Wizard"); - Set cTypes = card.getType().getCreatureTypes(); - for (String t : partyTypes) { - if (cTypes.contains(t)) { - isParty = true; - break; - } - } - return isParty; } else if (property.startsWith("sharesCreatureTypeWith")) { if (property.equals("sharesCreatureTypeWith")) { if (!card.sharesCreatureTypeWith(source)) {