Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
110 changes: 105 additions & 5 deletions forge-ai/src/main/java/forge/ai/ComputerUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -2067,22 +2067,122 @@ public static boolean playImmediately(Player ai, SpellAbility sa) {
return false;
}

public static int scoreHand(CardCollectionView handList, Player ai, int cardsToReturn) {
public static List<CardCollectionView> combinationsWithRemoved(CardCollectionView handList, int cardsToReturn) {
int handSize = handList.size();
int keepCount = handSize - cardsToReturn;

// Tracks which cards are in the current kept combination.
// Starts as [0, 1, ..., keepCount-1] and advances lexicographically
// through all C(handSize, keepCount) combinations.
int[] indices = new int[keepCount];
for (int i = 0; i < keepCount; i++) {
indices[i] = i;
}

// Keyed on sorted card names to deduplicate combinations involving multiple
// copies of the same card, e.g. keeping [Lightning Bolt, Island] is the same
// regardless of which Island was picked.
Set<String> seen = new HashSet<>();
List<CardCollectionView> result = new ArrayList<>();

do {
CardCollection kept = new CardCollection();
for (int idx : indices) {
kept.add(handList.get(idx));
}

// Only add to combinations if we haven't seen this already.
String key = kept.stream().map(Card::getName).sorted().toList().toString();
if (seen.add(key)) {
CardCollection removed = new CardCollection(handList);
removed.removeAll(kept);
result.add(removed);
}
} while (advanceIndices(indices, handSize));

return result;
}

// Advances indices to the next combination in lexicographic order.
// Returns false if all combinations have been exhausted.
// e.g. with handSize=7, keepCount=3: [2, 5, 6] -> [3, 4, 5] -> [3, 4, 6] -> ... -> [4, 5, 6] -> false
private static boolean advanceIndices(int[] indices, int handSize) {
int keepCount = indices.length;

// Find the rightmost index that can still be incremented
// (index at position i has max value of handSize - keepCount + i)
int i = keepCount - 1;
while (i >= 0 && indices[i] == handSize - keepCount + i) {
i--;
}
if (i < 0) return false;

// Increment that index, then reset everything to its right to be consecutive
indices[i]++;
for (int j = i + 1; j < keepCount; j++) {
indices[j] = indices[j - 1] + 1;
}
return true;
}

public static CardCollectionView chooseBestCardsToReturn(Player mulliganingPlayer, int cardsToReturn) {
CardCollectionView hand = mulliganingPlayer.getCardsIn(ZoneType.Hand);

// Nothing to return, keep the full hand
if (cardsToReturn == 0) {
return CardCollection.EMPTY;
}

// Returning everything, no need to evaluate
if (cardsToReturn == hand.size()) {
return hand;
}

// Generate every possible set of cards we could return to the library,
// then find the one that leaves the best remaining hand
List<CardCollectionView> candidateRemovals = combinationsWithRemoved(hand, cardsToReturn);

CardCollectionView bestRemoval = null;
int bestScore = Integer.MIN_VALUE;

for (CardCollectionView removal : candidateRemovals) {
// Compute what the hand would look like after returning these cards
CardCollection keptHand = new CardCollection(hand);
keptHand.removeAll(removal);

// Score the kept hand: pass 0 for cardsToReturn since keptHand is already the final hand
int score = scoreHand(keptHand, mulliganingPlayer, 0);

if (score > bestScore) {
bestScore = score;
bestRemoval = removal;
}
}

return bestRemoval != null ? bestRemoval : CardCollection.EMPTY;
}

public static int scoreHand(CardCollectionView handList, Player player, int cardsToReturn) {
// TODO Improve hand scoring in relation to cards to return.
// If final hand size is 5, score a hand based on what that 5 would be.
// Or if this is really really fast, determine what the 5 would be based on scoring
// All of the possibilities

final AiController aic = ((PlayerControllerAi)ai.getController()).getAi();
int mulliganThreshold = 4; // Sensible default for humans
if (player.getController() instanceof PlayerControllerAi) {
AiController aic = ((PlayerControllerAi) player.getController()).getAi();
mulliganThreshold = aic.getIntProperty(AiProps.MULLIGAN_THRESHOLD);
}

int currentHandSize = handList.size();
int finalHandSize = currentHandSize - cardsToReturn;

// don't mulligan when already too low
if (finalHandSize < aic.getIntProperty(AiProps.MULLIGAN_THRESHOLD)) {
if (finalHandSize < mulliganThreshold) {
return finalHandSize;
}

CardCollectionView library = ai.getCardsIn(ZoneType.Library);
CardCollectionView library = player.getCardsIn(ZoneType.Library);
int landsInDeck = CardLists.count(library, CardPredicates.LANDS);

// no land deck, can't do anything better
Expand Down Expand Up @@ -2112,7 +2212,7 @@ public static int scoreHand(CardCollectionView handList, Player ai, int cardsToR
// Improve score for perceived mana efficiency of the hand

// if at mulligan threshold, and we have any lands accept the hand
if (handSize == aic.getIntProperty(AiProps.MULLIGAN_THRESHOLD) && landSize > 0) {
if ((handSize == mulliganThreshold) && landSize > 0) {
return score;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
import forge.player.PlayerControllerHuman;
import forge.util.ITriggerEvent;
import forge.util.Localizer;
import forge.ai.ComputerUtil;

import java.util.List;

Expand Down Expand Up @@ -81,18 +82,14 @@ protected final void onOk() {

@Override
protected final void onCancel() {
int cardsLeft = toReturn - selected.size();
int count = 0;
for(Card c : player.getZone(ZoneType.Hand).getCards()) {
// Use the AI to determine the best cards to return to the library
CardCollectionView toMulligan = ComputerUtil.chooseBestCardsToReturn(player, toReturn);

for (Card c : toMulligan) {
if (selected.contains(c)) { continue; }

selected.add(c);
setCardHighlight(c, selected.contains(c));
count++;

if (cardsLeft == count) {
break;
}
setCardHighlight(c, true);
}

onOk();
Expand Down