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.
This commit is contained in:
Agetian
2024-04-21 17:20:59 +03:00
committed by GitHub
parent 648105f49b
commit 40c7bccd39
7 changed files with 164 additions and 11 deletions

View File

@@ -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 -->

View File

@@ -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<PaperCard> 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<PaperCard> main = deck.get(DeckSection.Main).toFlatList();
List<PaperCard> 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<PaperCard> processed = Lists.newArrayList();
Map<PaperCard, PaperCard> 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<PaperCard, PaperCard> 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<GameEntity, Integer> divideShield(Card effectSource, Map<GameEntity, Integer> 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<>();
}

View File

@@ -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()) {

View File

@@ -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
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

View File

@@ -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
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

View File

@@ -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 --

View File

@@ -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
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