mirror of
https://github.com/Card-Forge/forge.git
synced 2025-11-15 18:28:00 +00:00
Deck-based AI hints metadata + predefined sideboarding plan support (#5113)
* - Support for AI hints in deck metadata. - Support for pre-planned sideboarding using an AI hint. * - Fix imports. * - NPE prevention for cases when the AI has no sideboard.
This commit is contained in:
@@ -102,10 +102,43 @@ 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) {
|
||||||
if (!getAi().getBooleanProperty(AiProps.SIDEBOARDING_ENABLE)) {
|
if (!getAi().getBooleanProperty(AiProps.SIDEBOARDING_ENABLE)
|
||||||
|
|| !deck.has(DeckSection.Sideboard)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Map<PaperCard, PaperCard> sideboardPlan = Maps.newHashMap();
|
||||||
|
List<PaperCard> main = deck.get(DeckSection.Main).toFlatList();
|
||||||
|
List<PaperCard> 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 sbLimitedFormats = getAi().getBooleanProperty(AiProps.SIDEBOARDING_IN_LIMITED_FORMATS);
|
||||||
boolean sbSharedTypesOnly = getAi().getBooleanProperty(AiProps.SIDEBOARDING_SHARED_TYPE_ONLY);
|
boolean sbSharedTypesOnly = getAi().getBooleanProperty(AiProps.SIDEBOARDING_SHARED_TYPE_ONLY);
|
||||||
boolean sbPlaneswalkerException = getAi().getBooleanProperty(AiProps.SIDEBOARDING_PLANESWALKER_EQ_CREATURE);
|
boolean sbPlaneswalkerException = getAi().getBooleanProperty(AiProps.SIDEBOARDING_PLANESWALKER_EQ_CREATURE);
|
||||||
@@ -122,63 +155,60 @@ public class PlayerControllerAi extends PlayerController {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
List<PaperCard> main = deck.get(DeckSection.Main).toFlatList();
|
|
||||||
List<PaperCard> sideboard = deck.get(DeckSection.Sideboard).toFlatList();
|
|
||||||
|
|
||||||
// Devise a sideboarding plan
|
// Devise a sideboarding plan
|
||||||
// TODO: Allow a pre-planned (e.g. fully transformative) sideboard via deck metadata of some kind?
|
if (!definedSideboardPlan) {
|
||||||
List<PaperCard> processed = Lists.newArrayList();
|
List<PaperCard> processed = Lists.newArrayList();
|
||||||
Map<PaperCard, PaperCard> sideboardPlan = Maps.newHashMap();
|
for (PaperCard cSide : sideboard) {
|
||||||
for (PaperCard cSide : sideboard) {
|
if (processed.contains(cSide)) {
|
||||||
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;
|
continue;
|
||||||
} else if (cMain.getName().equals(cSide.getName())) {
|
} else if (cSide.getRules().getAiHints().getRemAIDecks()) {
|
||||||
continue;
|
continue; // don't sideboard in anything that we don't know how to play
|
||||||
} else if (cMain.getRules().getType().isLand()) {
|
} else if (cSide.getRules().getType().isLand()) {
|
||||||
continue; // don't know how to sideboard lands efficiently yet
|
continue; // don't know how to sideboard lands efficiently yet
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sbSharedTypesOnly) {
|
for (PaperCard cMain : main) {
|
||||||
if (!cMain.getRules().getType().sharesCardTypeWith(cSide.getRules().getType())) {
|
if (processed.contains(cMain)) {
|
||||||
continue; // Only equivalent types allowed
|
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())
|
if (sbSharedTypesOnly) {
|
||||||
|| (cSide.getRules().getType().isCreature()) && !cMain.getRules().getType().isCreature()) {
|
if (!cMain.getRules().getType().sharesCardTypeWith(cSide.getRules().getType())) {
|
||||||
if (!(sbPlaneswalkerException && (cMain.getRules().getType().isPlaneswalker() || cSide.getRules().getType().isPlaneswalker()))) {
|
continue; // Only equivalent types allowed
|
||||||
continue; // Creature exception: only trade a creature for another creature unless planeswalkers are allowed as a replacement
|
}
|
||||||
|
} 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()) {
|
if (!Card.fromPaperCard(cMain, player).getManaAbilities().isEmpty()) {
|
||||||
processed.add(cMain);
|
processed.add(cMain);
|
||||||
continue; // Mana Ability exception: Don't sideboard out cards that produce mana, can screw up the mana base
|
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
|
// Try not to screw up the mana curve or color distribution too much
|
||||||
if (cSide.getRules().getColor().hasNoColorsExcept(cMain.getRules().getColor())
|
if (cSide.getRules().getColor().hasNoColorsExcept(cMain.getRules().getColor())
|
||||||
&& cMain.getRules().getManaCost().getCMC() == cSide.getRules().getManaCost().getCMC()) {
|
&& cMain.getRules().getManaCost().getCMC() == cSide.getRules().getManaCost().getCMC()) {
|
||||||
sideboardPlan.put(cMain, cSide);
|
sideboardPlan.put(cMain, cSide);
|
||||||
processed.add(cSide);
|
processed.add(cSide);
|
||||||
processed.add(cMain);
|
processed.add(cMain);
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Make changes according to the sideboarding plan suggested above
|
// Make changes according to the sideboarding plan suggested above
|
||||||
for (Map.Entry<PaperCard, PaperCard> ent : sideboardPlan.entrySet()) {
|
for (Map.Entry<PaperCard, PaperCard> ent : sideboardPlan.entrySet()) {
|
||||||
if (MyRandom.getRandom().nextInt(100) < sbChancePerCard) {
|
if (!definedSideboardPlan && MyRandom.getRandom().nextInt(100) < sbChancePerCard) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
long inMain = main.stream().filter(pc -> pc.getCardName().equals(ent.getKey().getName())).count();
|
long inMain = main.stream().filter(pc -> pc.getCardName().equals(ent.getKey().getName())).count();
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ public class Deck extends DeckBase implements Iterable<Entry<DeckSection, CardPo
|
|||||||
private final Set<String> tags = new TreeSet<>(String.CASE_INSENSITIVE_ORDER);
|
private final Set<String> tags = new TreeSet<>(String.CASE_INSENSITIVE_ORDER);
|
||||||
// Supports deferring loading a deck until we actually need its contents. This works in conjunction with
|
// 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.
|
// the lazy card load feature to ensure we don't need to load all cards on start up.
|
||||||
|
private final Set<String> aiHints = new TreeSet<>();
|
||||||
private Map<String, List<String>> deferredSections = null;
|
private Map<String, List<String>> deferredSections = null;
|
||||||
private Map<String, List<String>> loadedSections = null;
|
private Map<String, List<String>> loadedSections = null;
|
||||||
private String lastCardArtPreferenceUsed = "";
|
private String lastCardArtPreferenceUsed = "";
|
||||||
@@ -207,6 +208,7 @@ public class Deck extends DeckBase implements Iterable<Entry<DeckSection, CardPo
|
|||||||
result.parts.put(kv.getKey(), cp);
|
result.parts.put(kv.getKey(), cp);
|
||||||
cp.addAll(kv.getValue());
|
cp.addAll(kv.getValue());
|
||||||
}
|
}
|
||||||
|
result.setAiHints(StringUtils.join(aiHints, " | "));
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -536,6 +538,29 @@ public class Deck extends DeckBase implements Iterable<Entry<DeckSection, CardPo
|
|||||||
return allCards;
|
return allCards;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setAiHints(String aiHintsInfo) {
|
||||||
|
if (aiHintsInfo == null || aiHintsInfo.trim().equals("")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
String[] hints = aiHintsInfo.split("\\|");
|
||||||
|
for (String hint : hints) {
|
||||||
|
aiHints.add(hint.trim());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Set<String> 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() {
|
public UnplayableAICards getUnplayableAICards() {
|
||||||
if (unplayableAI == null) {
|
if (unplayableAI == null) {
|
||||||
unplayableAI = new UnplayableAICards(this);
|
unplayableAI = new UnplayableAICards(this);
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ public class DeckFileHeader {
|
|||||||
private static final String PLAYER = "Player";
|
private static final String PLAYER = "Player";
|
||||||
private static final String CSTM_POOL = "Custom Pool";
|
private static final String CSTM_POOL = "Custom Pool";
|
||||||
private static final String PLAYER_TYPE = "PlayerType";
|
private static final String PLAYER_TYPE = "PlayerType";
|
||||||
|
public static final String AI_HINTS = "AiHints";
|
||||||
|
|
||||||
private final DeckFormat deckType;
|
private final DeckFormat deckType;
|
||||||
private final boolean customPool;
|
private final boolean customPool;
|
||||||
@@ -55,6 +56,7 @@ public class DeckFileHeader {
|
|||||||
private final Set<String> tags;
|
private final Set<String> tags;
|
||||||
|
|
||||||
private final boolean intendedForAi;
|
private final boolean intendedForAi;
|
||||||
|
private final String aiHints;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return the intendedForAi
|
* @return the intendedForAi
|
||||||
@@ -63,6 +65,13 @@ public class DeckFileHeader {
|
|||||||
return intendedForAi;
|
return intendedForAi;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return the AI hints
|
||||||
|
*/
|
||||||
|
public String getAiHints() {
|
||||||
|
return aiHints;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* TODO: Write javadoc for Constructor.
|
* TODO: Write javadoc for Constructor.
|
||||||
*
|
*
|
||||||
@@ -75,6 +84,7 @@ public class DeckFileHeader {
|
|||||||
this.deckType = DeckFormat.smartValueOf(kvPairs.get(DeckFileHeader.DECK_TYPE), DeckFormat.Constructed);
|
this.deckType = DeckFormat.smartValueOf(kvPairs.get(DeckFileHeader.DECK_TYPE), DeckFormat.Constructed);
|
||||||
this.customPool = kvPairs.getBoolean(DeckFileHeader.CSTM_POOL);
|
this.customPool = kvPairs.getBoolean(DeckFileHeader.CSTM_POOL);
|
||||||
this.intendedForAi = "computer".equalsIgnoreCase(kvPairs.get(DeckFileHeader.PLAYER)) || "ai".equalsIgnoreCase(kvPairs.get(DeckFileHeader.PLAYER_TYPE));
|
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<>();
|
this.tags = new TreeSet<>();
|
||||||
|
|
||||||
String rawTags = kvPairs.get(DeckFileHeader.TAGS);
|
String rawTags = kvPairs.get(DeckFileHeader.TAGS);
|
||||||
|
|||||||
@@ -53,6 +53,9 @@ public class DeckSerializer {
|
|||||||
if (!d.getTags().isEmpty()) {
|
if (!d.getTags().isEmpty()) {
|
||||||
out.add(TextUtil.concatNoSpace(DeckFileHeader.TAGS,"=", StringUtils.join(d.getTags(), DeckFileHeader.TAGS_SEPARATOR)));
|
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<DeckSection, CardPool> s : d) {
|
for(Entry<DeckSection, CardPool> s : d) {
|
||||||
out.add(TextUtil.enclosedBracket(s.getKey().toString()));
|
out.add(TextUtil.enclosedBracket(s.getKey().toString()));
|
||||||
@@ -77,6 +80,7 @@ public class DeckSerializer {
|
|||||||
|
|
||||||
Deck d = new Deck(dh.getName());
|
Deck d = new Deck(dh.getName());
|
||||||
d.setComment(dh.getComment());
|
d.setComment(dh.getComment());
|
||||||
|
d.setAiHints(dh.getAiHints());
|
||||||
d.getTags().addAll(dh.getTags());
|
d.getTags().addAll(dh.getTags());
|
||||||
d.setDeferredSections(sections);
|
d.setDeferredSections(sections);
|
||||||
return d;
|
return d;
|
||||||
|
|||||||
Reference in New Issue
Block a user