diff --git a/forge-ai/src/main/java/forge/ai/PlayerControllerAi.java b/forge-ai/src/main/java/forge/ai/PlayerControllerAi.java index 3686e6d6430..a60e25b450b 100644 --- a/forge-ai/src/main/java/forge/ai/PlayerControllerAi.java +++ b/forge-ai/src/main/java/forge/ai/PlayerControllerAi.java @@ -102,10 +102,43 @@ public class PlayerControllerAi extends PlayerController { @Override public List sideboard(Deck deck, GameType gameType, String message) { - if (!getAi().getBooleanProperty(AiProps.SIDEBOARDING_ENABLE)) { + if (!getAi().getBooleanProperty(AiProps.SIDEBOARDING_ENABLE) + || !deck.has(DeckSection.Sideboard)) { return null; } + Map sideboardPlan = Maps.newHashMap(); + List main = deck.get(DeckSection.Main).toFlatList(); + List sideboard = deck.get(DeckSection.Sideboard).toFlatList(); + + // Predefined sideboard plan from deck metadata (AI hints) + boolean definedSideboardPlan = false; + String sideboardAiHint = deck.getAiHint("SideboardingPlan"); + if (!sideboardAiHint.isEmpty()) { + for (String element : sideboardAiHint.split(";")) { + String[] cardPair = element.split("->"); + PaperCard src = null, tgt = null; + for (PaperCard cMain : main) { + if (cMain.getCardName().equals(cardPair[0].trim())) { + src = cMain; + break; + } + } + for (PaperCard cSide : sideboard) { + if (cSide.getCardName().equals(cardPair[1].trim())) { + tgt = cSide; + break; + } + } + if (src != null && tgt != null) { + sideboardPlan.put(src, tgt); + } + } + if (!sideboardPlan.isEmpty()) { + definedSideboardPlan = true; + } + } + boolean sbLimitedFormats = getAi().getBooleanProperty(AiProps.SIDEBOARDING_IN_LIMITED_FORMATS); boolean sbSharedTypesOnly = getAi().getBooleanProperty(AiProps.SIDEBOARDING_SHARED_TYPE_ONLY); boolean sbPlaneswalkerException = getAi().getBooleanProperty(AiProps.SIDEBOARDING_PLANESWALKER_EQ_CREATURE); @@ -122,63 +155,60 @@ public class PlayerControllerAi extends PlayerController { return null; } - List main = deck.get(DeckSection.Main).toFlatList(); - List sideboard = deck.get(DeckSection.Sideboard).toFlatList(); - // Devise a sideboarding plan - // TODO: Allow a pre-planned (e.g. fully transformative) sideboard via deck metadata of some kind? - List processed = Lists.newArrayList(); - Map sideboardPlan = Maps.newHashMap(); - for (PaperCard cSide : sideboard) { - if (processed.contains(cSide)) { - continue; - } else if (cSide.getRules().getAiHints().getRemAIDecks()) { - continue; // don't sideboard in anything that we don't know how to play - } else if (cSide.getRules().getType().isLand()) { - continue; // don't know how to sideboard lands efficiently yet - } - - for (PaperCard cMain : main) { - if (processed.contains(cMain)) { + if (!definedSideboardPlan) { + List processed = Lists.newArrayList(); + for (PaperCard cSide : sideboard) { + if (processed.contains(cSide)) { continue; - } else if (cMain.getName().equals(cSide.getName())) { - continue; - } else if (cMain.getRules().getType().isLand()) { + } else if (cSide.getRules().getAiHints().getRemAIDecks()) { + continue; // don't sideboard in anything that we don't know how to play + } else if (cSide.getRules().getType().isLand()) { continue; // don't know how to sideboard lands efficiently yet } - if (sbSharedTypesOnly) { - if (!cMain.getRules().getType().sharesCardTypeWith(cSide.getRules().getType())) { - continue; // Only equivalent types allowed + for (PaperCard cMain : main) { + if (processed.contains(cMain)) { + continue; + } else if (cMain.getName().equals(cSide.getName())) { + continue; + } else if (cMain.getRules().getType().isLand()) { + continue; // don't know how to sideboard lands efficiently yet } - } else { - if ((cMain.getRules().getType().isCreature() && !cSide.getRules().getType().isCreature()) - || (cSide.getRules().getType().isCreature()) && !cMain.getRules().getType().isCreature()) { - if (!(sbPlaneswalkerException && (cMain.getRules().getType().isPlaneswalker() || cSide.getRules().getType().isPlaneswalker()))) { - continue; // Creature exception: only trade a creature for another creature unless planeswalkers are allowed as a replacement + + if (sbSharedTypesOnly) { + if (!cMain.getRules().getType().sharesCardTypeWith(cSide.getRules().getType())) { + continue; // Only equivalent types allowed + } + } else { + if ((cMain.getRules().getType().isCreature() && !cSide.getRules().getType().isCreature()) + || (cSide.getRules().getType().isCreature()) && !cMain.getRules().getType().isCreature()) { + if (!(sbPlaneswalkerException && (cMain.getRules().getType().isPlaneswalker() || cSide.getRules().getType().isPlaneswalker()))) { + continue; // Creature exception: only trade a creature for another creature unless planeswalkers are allowed as a replacement + } } } - } - if (!Card.fromPaperCard(cMain, player).getManaAbilities().isEmpty()) { - processed.add(cMain); - continue; // Mana Ability exception: Don't sideboard out cards that produce mana, can screw up the mana base - } + if (!Card.fromPaperCard(cMain, player).getManaAbilities().isEmpty()) { + processed.add(cMain); + continue; // Mana Ability exception: Don't sideboard out cards that produce mana, can screw up the mana base + } - // Try not to screw up the mana curve or color distribution too much - if (cSide.getRules().getColor().hasNoColorsExcept(cMain.getRules().getColor()) - && cMain.getRules().getManaCost().getCMC() == cSide.getRules().getManaCost().getCMC()) { - sideboardPlan.put(cMain, cSide); - processed.add(cSide); - processed.add(cMain); - break; + // Try not to screw up the mana curve or color distribution too much + if (cSide.getRules().getColor().hasNoColorsExcept(cMain.getRules().getColor()) + && cMain.getRules().getManaCost().getCMC() == cSide.getRules().getManaCost().getCMC()) { + sideboardPlan.put(cMain, cSide); + processed.add(cSide); + processed.add(cMain); + break; + } } } } // Make changes according to the sideboarding plan suggested above for (Map.Entry ent : sideboardPlan.entrySet()) { - if (MyRandom.getRandom().nextInt(100) < sbChancePerCard) { + if (!definedSideboardPlan && MyRandom.getRandom().nextInt(100) < sbChancePerCard) { continue; } long inMain = main.stream().filter(pc -> pc.getCardName().equals(ent.getKey().getName())).count(); diff --git a/forge-core/src/main/java/forge/deck/Deck.java b/forge-core/src/main/java/forge/deck/Deck.java index c91d95f333f..c0b3ec5aecc 100644 --- a/forge-core/src/main/java/forge/deck/Deck.java +++ b/forge-core/src/main/java/forge/deck/Deck.java @@ -48,6 +48,7 @@ public class Deck extends DeckBase implements Iterable tags = new TreeSet<>(String.CASE_INSENSITIVE_ORDER); // Supports deferring loading a deck until we actually need its contents. This works in conjunction with // the lazy card load feature to ensure we don't need to load all cards on start up. + private final Set aiHints = new TreeSet<>(); private Map> deferredSections = null; private Map> loadedSections = null; private String lastCardArtPreferenceUsed = ""; @@ -207,6 +208,7 @@ public class Deck extends DeckBase implements Iterable getAiHints() { + return aiHints; + } + + public String getAiHint(String name) { + for (String aiHint : aiHints) { + if (aiHint.toLowerCase().startsWith(name.toLowerCase() + "$")) { + return aiHint.substring(aiHint.indexOf("$") + 1).trim(); + } + } + return ""; + } + public UnplayableAICards getUnplayableAICards() { if (unplayableAI == null) { unplayableAI = new UnplayableAICards(this); diff --git a/forge-core/src/main/java/forge/deck/io/DeckFileHeader.java b/forge-core/src/main/java/forge/deck/io/DeckFileHeader.java index 7c90fe4dfec..2d630ccb191 100644 --- a/forge-core/src/main/java/forge/deck/io/DeckFileHeader.java +++ b/forge-core/src/main/java/forge/deck/io/DeckFileHeader.java @@ -45,6 +45,7 @@ public class DeckFileHeader { private static final String PLAYER = "Player"; private static final String CSTM_POOL = "Custom Pool"; private static final String PLAYER_TYPE = "PlayerType"; + public static final String AI_HINTS = "AiHints"; private final DeckFormat deckType; private final boolean customPool; @@ -55,6 +56,7 @@ public class DeckFileHeader { private final Set tags; private final boolean intendedForAi; + private final String aiHints; /** * @return the intendedForAi @@ -63,6 +65,13 @@ public class DeckFileHeader { return intendedForAi; } + /** + * @return the AI hints + */ + public String getAiHints() { + return aiHints; + } + /** * TODO: Write javadoc for Constructor. * @@ -75,6 +84,7 @@ public class DeckFileHeader { this.deckType = DeckFormat.smartValueOf(kvPairs.get(DeckFileHeader.DECK_TYPE), DeckFormat.Constructed); this.customPool = kvPairs.getBoolean(DeckFileHeader.CSTM_POOL); this.intendedForAi = "computer".equalsIgnoreCase(kvPairs.get(DeckFileHeader.PLAYER)) || "ai".equalsIgnoreCase(kvPairs.get(DeckFileHeader.PLAYER_TYPE)); + this.aiHints = kvPairs.get(DeckFileHeader.AI_HINTS); this.tags = new TreeSet<>(); String rawTags = kvPairs.get(DeckFileHeader.TAGS); diff --git a/forge-core/src/main/java/forge/deck/io/DeckSerializer.java b/forge-core/src/main/java/forge/deck/io/DeckSerializer.java index 95fe2feaca0..587c4efe81b 100644 --- a/forge-core/src/main/java/forge/deck/io/DeckSerializer.java +++ b/forge-core/src/main/java/forge/deck/io/DeckSerializer.java @@ -53,6 +53,9 @@ public class DeckSerializer { if (!d.getTags().isEmpty()) { out.add(TextUtil.concatNoSpace(DeckFileHeader.TAGS,"=", StringUtils.join(d.getTags(), DeckFileHeader.TAGS_SEPARATOR))); } + if (!d.getAiHints().isEmpty()) { + out.add(TextUtil.concatNoSpace(DeckFileHeader.AI_HINTS, "=", StringUtils.join(d.getAiHints(), " | "))); + } for(Entry s : d) { out.add(TextUtil.enclosedBracket(s.getKey().toString())); @@ -77,6 +80,7 @@ public class DeckSerializer { Deck d = new Deck(dh.getName()); d.setComment(dh.getComment()); + d.setAiHints(dh.getAiHints()); d.getTags().addAll(dh.getTags()); d.setDeferredSections(sections); return d;