From 40c7bccd3937ae88795f9dcbc7910d2eb8a9d0e1 Mon Sep 17 00:00:00 2001 From: Agetian Date: Sun, 21 Apr 2024 17:20:59 +0300 Subject: [PATCH] Basic AI sideboarding framework (#5089) * - Add OTJ achievements by Marek14. * - AI hint for Transcendence. * - Basic AI sideboarding framework. * - Basic AI sideboarding framework. * - Add an option to allow planeswalkers to replace creatures. * - Add an option to only allow shared types for cards exchanged during sideboarding. * - Comment tweak * - Modifications according to recommendations. * - Logic tweak for lastOutcome. --- forge-ai/src/main/java/forge/ai/AiProps.java | 8 +- .../java/forge/ai/PlayerControllerAi.java | 101 ++++++++++++++++-- .../src/main/java/forge/game/Match.java | 4 + forge-gui/res/ai/Cautious.ai | 16 ++- forge-gui/res/ai/Default.ai | 16 ++- forge-gui/res/ai/Experimental.ai | 14 +++ forge-gui/res/ai/Reckless.ai | 16 ++- 7 files changed, 164 insertions(+), 11 deletions(-) diff --git a/forge-ai/src/main/java/forge/ai/AiProps.java b/forge-ai/src/main/java/forge/ai/AiProps.java index 2edeacba908..27fc0658a30 100644 --- a/forge-ai/src/main/java/forge/ai/AiProps.java +++ b/forge-ai/src/main/java/forge/ai/AiProps.java @@ -139,7 +139,13 @@ public enum AiProps { /** */ SACRIFICE_DEFAULT_PREF_MIN_CMC("0"), SACRIFICE_DEFAULT_PREF_MAX_CMC("2"), SACRIFICE_DEFAULT_PREF_ALLOW_TOKENS("true"), - SACRIFICE_DEFAULT_PREF_MAX_CREATURE_EVAL("135"); + SACRIFICE_DEFAULT_PREF_MAX_CREATURE_EVAL("135"), + SIDEBOARDING_ENABLE("true"), + SIDEBOARDING_CHANCE_PER_CARD("50"), + SIDEBOARDING_CHANCE_ON_WIN("0"), + SIDEBOARDING_IN_LIMITED_FORMATS("false"), + SIDEBOARDING_SHARED_TYPE_ONLY("false"), + SIDEBOARDING_PLANESWALKER_EQ_CREATURE("false"); // Experimental features, must be promoted or removed after extensive testing and, ideally, defaulting // <-- There are no experimental options here --> diff --git a/forge-ai/src/main/java/forge/ai/PlayerControllerAi.java b/forge-ai/src/main/java/forge/ai/PlayerControllerAi.java index e1aab6af645..3686e6d6430 100644 --- a/forge-ai/src/main/java/forge/ai/PlayerControllerAi.java +++ b/forge-ai/src/main/java/forge/ai/PlayerControllerAi.java @@ -2,10 +2,7 @@ package forge.ai; import com.google.common.base.Predicate; import com.google.common.base.Predicates; -import com.google.common.collect.Iterables; -import com.google.common.collect.ListMultimap; -import com.google.common.collect.Lists; -import com.google.common.collect.Multimap; +import com.google.common.collect.*; import forge.LobbyPlayer; import forge.ai.ability.ProtectAi; import forge.card.CardStateName; @@ -105,8 +102,98 @@ public class PlayerControllerAi extends PlayerController { @Override public List sideboard(Deck deck, GameType gameType, String message) { - // AI does not know how to sideboard - return null; + if (!getAi().getBooleanProperty(AiProps.SIDEBOARDING_ENABLE)) { + return null; + } + + 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); + int sbChanceOnWin = getAi().getIntProperty(AiProps.SIDEBOARDING_CHANCE_ON_WIN); + int sbChancePerCard = getAi().getIntProperty(AiProps.SIDEBOARDING_CHANCE_PER_CARD); + + if (!sbLimitedFormats && gameType.isCardPoolLimited()) { + return null; + } + + GameOutcome lastOutcome = brains.getGame().getMatch().getLastOutcome(); + if (lastOutcome.getWinningPlayer().getPlayer().equals(player.getLobbyPlayer()) + && MyRandom.getRandom().nextInt(100) > sbChanceOnWin) { + 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)) { + 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 + } + + 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 + } + + // 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) { + continue; + } + long inMain = main.stream().filter(pc -> pc.getCardName().equals(ent.getKey().getName())).count(); + long inSide = sideboard.stream().filter(pc -> pc.getCardName().equals(ent.getValue().getName())).count(); + while (inMain-- > 0 && inSide-- > 0) { + sideboard.remove(ent.getValue()); + sideboard.add(ent.getKey()); + main.add(ent.getValue()); + main.remove(ent.getKey()); + } + } + + // Return the new Main. It's important to make sure that the overall content of the deck (Main+Sideboard) + // does not change above, or the AI may cheat (sneak some cards in or remove them from the deck altogether). + return main; } @Override @@ -116,7 +203,7 @@ public class PlayerControllerAi extends PlayerController { @Override public Map divideShield(Card effectSource, Map affected, int shieldAmount) { - // TODO: AI current can't use this so this is not implemented. + // TODO: AI currently can't use this so this is not implemented. return new HashMap<>(); } diff --git a/forge-game/src/main/java/forge/game/Match.java b/forge-game/src/main/java/forge/game/Match.java index 15f5cfd06f0..9bf2761d4ed 100644 --- a/forge-game/src/main/java/forge/game/Match.java +++ b/forge-game/src/main/java/forge/game/Match.java @@ -117,6 +117,10 @@ public class Match { return gameOutcomes.values(); } + public GameOutcome getLastOutcome() { + return lastOutcome; + } + public boolean isMatchOver() { int[] victories = new int[players.size()]; for (GameOutcome go : getOutcomes()) { diff --git a/forge-gui/res/ai/Cautious.ai b/forge-gui/res/ai/Cautious.ai index 9eb9b44bfc8..aab608a66b0 100644 --- a/forge-gui/res/ai/Cautious.ai +++ b/forge-gui/res/ai/Cautious.ai @@ -311,4 +311,18 @@ SACRIFICE_DEFAULT_PREF_MAX_CMC=1 # consider the sacrifice of a matching card is a token SACRIFICE_DEFAULT_PREF_ALLOW_TOKENS=true # A creature should evaluate to no more than this much to be considered for default SacCost preference -SACRIFICE_DEFAULT_PREF_MAX_CREATURE_EVAL=135 \ No newline at end of file +SACRIFICE_DEFAULT_PREF_MAX_CREATURE_EVAL=135 + +# Master toggle enabling AI sideboarding +SIDEBOARDING_ENABLE=true +# Enable sideboarding in limited formats (e.g. Sealed, Draft) +SIDEBOARDING_IN_LIMITED_FORMATS=false +# Chance to proceed with sideboarding any given pair of cards in the devised sideboarding plan +SIDEBOARDING_CHANCE_PER_CARD=50 +# Chance to do some sideboarding after winning a game +SIDEBOARDING_CHANCE_ON_WIN=0 +# Only allow replacing a card with another card that shares the same core types. If disabled, mixing types will be +# allowed, although a creature is still only replaced with another creature (or planeswalker, see the next option) +SIDEBOARDING_SHARED_TYPE_ONLY=true +# Allow replacing a creature with a planeswalker and vice versa when sideboarding +SIDEBOARDING_PLANESWALKER_EQ_CREATURE=false diff --git a/forge-gui/res/ai/Default.ai b/forge-gui/res/ai/Default.ai index d4e1bcc8763..8415bd2c29c 100644 --- a/forge-gui/res/ai/Default.ai +++ b/forge-gui/res/ai/Default.ai @@ -312,4 +312,18 @@ SACRIFICE_DEFAULT_PREF_MAX_CMC=2 # consider the sacrifice of a matching card is a token SACRIFICE_DEFAULT_PREF_ALLOW_TOKENS=true # A creature should evaluate to no more than this much to be considered for default SacCost preference -SACRIFICE_DEFAULT_PREF_MAX_CREATURE_EVAL=135 \ No newline at end of file +SACRIFICE_DEFAULT_PREF_MAX_CREATURE_EVAL=135 + +# Master toggle enabling AI sideboarding +SIDEBOARDING_ENABLE=true +# Enable sideboarding in limited formats (e.g. Sealed, Draft) +SIDEBOARDING_IN_LIMITED_FORMATS=false +# Chance to proceed with sideboarding any given pair of cards in the devised sideboarding plan +SIDEBOARDING_CHANCE_PER_CARD=50 +# Chance to do some sideboarding after winning a game +SIDEBOARDING_CHANCE_ON_WIN=0 +# Only allow replacing a card with another card that shares the same core types. If disabled, mixing types will be +# allowed, although a creature is still only replaced with another creature (or planeswalker, see the next option) +SIDEBOARDING_SHARED_TYPE_ONLY=false +# Allow replacing a creature with a planeswalker and vice versa when sideboarding if the previous option is disabled +SIDEBOARDING_PLANESWALKER_EQ_CREATURE=false \ No newline at end of file diff --git a/forge-gui/res/ai/Experimental.ai b/forge-gui/res/ai/Experimental.ai index 3d05b03bf4f..286f9d345f6 100644 --- a/forge-gui/res/ai/Experimental.ai +++ b/forge-gui/res/ai/Experimental.ai @@ -314,6 +314,20 @@ SACRIFICE_DEFAULT_PREF_ALLOW_TOKENS=true # A creature should evaluate to no more than this much to be considered for default SacCost preference SACRIFICE_DEFAULT_PREF_MAX_CREATURE_EVAL=135 +# Master toggle enabling AI sideboarding +SIDEBOARDING_ENABLE=true +# Enable sideboarding in limited formats (e.g. Sealed, Draft) +SIDEBOARDING_IN_LIMITED_FORMATS=false +# Chance to proceed with sideboarding any given pair of cards in the devised sideboarding plan +SIDEBOARDING_CHANCE_PER_CARD=50 +# Chance to do some sideboarding after winning a game +SIDEBOARDING_CHANCE_ON_WIN=25 +# Only allow replacing a card with another card that shares the same core types. If disabled, mixing types will be +# allowed, although a creature is still only replaced with another creature (or planeswalker, see the next option) +SIDEBOARDING_SHARED_TYPE_ONLY=true +# Allow replacing a creature with a planeswalker and vice versa when sideboarding if the previous option is disabled +SIDEBOARDING_PLANESWALKER_EQ_CREATURE=true + # -- Experimental feature toggles which only exist until the testing procedure for the relevant -- # -- features is over. These toggles will be removed later, or may be reintroduced under a -- # -- different name if necessary -- diff --git a/forge-gui/res/ai/Reckless.ai b/forge-gui/res/ai/Reckless.ai index 4d092ff8029..fd398fb8506 100644 --- a/forge-gui/res/ai/Reckless.ai +++ b/forge-gui/res/ai/Reckless.ai @@ -312,4 +312,18 @@ SACRIFICE_DEFAULT_PREF_MAX_CMC=3 # consider the sacrifice of a matching card is a token SACRIFICE_DEFAULT_PREF_ALLOW_TOKENS=true # A creature should evaluate to no more than this much to be considered for default SacCost preference -SACRIFICE_DEFAULT_PREF_MAX_CREATURE_EVAL=135 \ No newline at end of file +SACRIFICE_DEFAULT_PREF_MAX_CREATURE_EVAL=135 + +# Master toggle enabling AI sideboarding +SIDEBOARDING_ENABLE=true +# Enable sideboarding in limited formats (e.g. Sealed, Draft) +SIDEBOARDING_IN_LIMITED_FORMATS=false +# Chance to proceed with sideboarding any given pair of cards in the devised sideboarding plan +SIDEBOARDING_CHANCE_PER_CARD=65 +# Chance to do some sideboarding after winning a game +SIDEBOARDING_CHANCE_ON_WIN=0 +# Only allow replacing a card with another card that shares the same core types. If disabled, mixing types will be +# allowed, although a creature is still only replaced with another creature (or planeswalker, see the next option) +SIDEBOARDING_SHARED_TYPE_ONLY=false +# Allow replacing a creature with a planeswalker and vice versa when sideboarding +SIDEBOARDING_PLANESWALKER_EQ_CREATURE=false