mirror of
https://github.com/Card-Forge/forge.git
synced 2025-11-19 04:08:01 +00:00
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:
@@ -139,7 +139,13 @@ public enum AiProps { /** */
|
|||||||
SACRIFICE_DEFAULT_PREF_MIN_CMC("0"),
|
SACRIFICE_DEFAULT_PREF_MIN_CMC("0"),
|
||||||
SACRIFICE_DEFAULT_PREF_MAX_CMC("2"),
|
SACRIFICE_DEFAULT_PREF_MAX_CMC("2"),
|
||||||
SACRIFICE_DEFAULT_PREF_ALLOW_TOKENS("true"),
|
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
|
// Experimental features, must be promoted or removed after extensive testing and, ideally, defaulting
|
||||||
// <-- There are no experimental options here -->
|
// <-- There are no experimental options here -->
|
||||||
|
|
||||||
|
|||||||
@@ -2,10 +2,7 @@ package forge.ai;
|
|||||||
|
|
||||||
import com.google.common.base.Predicate;
|
import com.google.common.base.Predicate;
|
||||||
import com.google.common.base.Predicates;
|
import com.google.common.base.Predicates;
|
||||||
import com.google.common.collect.Iterables;
|
import com.google.common.collect.*;
|
||||||
import com.google.common.collect.ListMultimap;
|
|
||||||
import com.google.common.collect.Lists;
|
|
||||||
import com.google.common.collect.Multimap;
|
|
||||||
import forge.LobbyPlayer;
|
import forge.LobbyPlayer;
|
||||||
import forge.ai.ability.ProtectAi;
|
import forge.ai.ability.ProtectAi;
|
||||||
import forge.card.CardStateName;
|
import forge.card.CardStateName;
|
||||||
@@ -105,10 +102,100 @@ public class PlayerControllerAi extends PlayerController {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<PaperCard> sideboard(Deck deck, GameType gameType, String message) {
|
public List<PaperCard> sideboard(Deck deck, GameType gameType, String message) {
|
||||||
// AI does not know how to sideboard
|
if (!getAi().getBooleanProperty(AiProps.SIDEBOARDING_ENABLE)) {
|
||||||
return null;
|
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
|
@Override
|
||||||
public Map<Card, Integer> assignCombatDamage(Card attacker, CardCollectionView blockers, CardCollectionView remaining, int damageDealt, GameEntity defender, boolean overrideOrder) {
|
public Map<Card, Integer> assignCombatDamage(Card attacker, CardCollectionView blockers, CardCollectionView remaining, int damageDealt, GameEntity defender, boolean overrideOrder) {
|
||||||
return ComputerUtilCombat.distributeAIDamage(player, attacker, blockers, remaining, damageDealt, defender, overrideOrder);
|
return ComputerUtilCombat.distributeAIDamage(player, attacker, blockers, remaining, damageDealt, defender, overrideOrder);
|
||||||
@@ -116,7 +203,7 @@ public class PlayerControllerAi extends PlayerController {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Map<GameEntity, Integer> divideShield(Card effectSource, Map<GameEntity, Integer> affected, int shieldAmount) {
|
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<>();
|
return new HashMap<>();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -117,6 +117,10 @@ public class Match {
|
|||||||
return gameOutcomes.values();
|
return gameOutcomes.values();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public GameOutcome getLastOutcome() {
|
||||||
|
return lastOutcome;
|
||||||
|
}
|
||||||
|
|
||||||
public boolean isMatchOver() {
|
public boolean isMatchOver() {
|
||||||
int[] victories = new int[players.size()];
|
int[] victories = new int[players.size()];
|
||||||
for (GameOutcome go : getOutcomes()) {
|
for (GameOutcome go : getOutcomes()) {
|
||||||
|
|||||||
@@ -312,3 +312,17 @@ SACRIFICE_DEFAULT_PREF_MAX_CMC=1
|
|||||||
SACRIFICE_DEFAULT_PREF_ALLOW_TOKENS=true
|
SACRIFICE_DEFAULT_PREF_ALLOW_TOKENS=true
|
||||||
# A creature should evaluate to no more than this much to be considered for default SacCost preference
|
# 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
|
||||||
|
|||||||
@@ -313,3 +313,17 @@ SACRIFICE_DEFAULT_PREF_MAX_CMC=2
|
|||||||
SACRIFICE_DEFAULT_PREF_ALLOW_TOKENS=true
|
SACRIFICE_DEFAULT_PREF_ALLOW_TOKENS=true
|
||||||
# A creature should evaluate to no more than this much to be considered for default SacCost preference
|
# 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
|
||||||
@@ -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
|
# 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=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 --
|
# -- 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 --
|
# -- features is over. These toggles will be removed later, or may be reintroduced under a --
|
||||||
# -- different name if necessary --
|
# -- different name if necessary --
|
||||||
|
|||||||
@@ -313,3 +313,17 @@ SACRIFICE_DEFAULT_PREF_MAX_CMC=3
|
|||||||
SACRIFICE_DEFAULT_PREF_ALLOW_TOKENS=true
|
SACRIFICE_DEFAULT_PREF_ALLOW_TOKENS=true
|
||||||
# A creature should evaluate to no more than this much to be considered for default SacCost preference
|
# 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
|
||||||
|
|||||||
Reference in New Issue
Block a user