commit 30fe12cee5b3876669f18a8ae9d6d63dc4955670
parent 722c8b51522c76e1a3e007b0ec0c458f719c6ff3
Author: Kevin Corvisier <git@kevincorvisier.fr>
Date: Thu, 24 Oct 2024 20:56:19 +0900
Card pool: allow limiting the number of available copies of cards
Diffstat:
6 files changed, 180 insertions(+), 17 deletions(-)
diff --git a/src/main/java/fr/kevincorvisier/mtg/gdb/ai/CardPoolService.java b/src/main/java/fr/kevincorvisier/mtg/gdb/ai/CardPoolService.java
@@ -4,14 +4,16 @@ import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
-import java.util.Collection;
-import java.util.HashSet;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import forge.deck.Deck;
-import forge.deck.DeckgenUtil;
import forge.game.GameFormat;
import forge.item.PaperCard;
import forge.model.FModel;
@@ -25,27 +27,52 @@ import lombok.extern.slf4j.Slf4j;
@RequiredArgsConstructor
public class CardPoolService implements Reloadable
{
+ private static final Pattern PATTERN_LINE = Pattern.compile("(?:(?<count>\\d+) )?(?<name>.+)");
+
@Value("${card-pool}")
private final File cardPool;
private final GameFormat format;
- private final Collection<String> cardNames = new HashSet<>();
+ private final Map<String, Integer> cards = new HashMap<>();
public String getRandomCard()
{
- return Aggregates.random(cardNames);
+ return Aggregates.random(cards.keySet());
+ }
+
+ public boolean contains(final PaperCard card)
+ {
+ return cards.keySet().contains(card.getName());
}
- public Deck getRandomDeck()
+ public int getMaxCount(final String cardName)
{
- return DeckgenUtil.getRandomColorDeck(card -> cardNames.contains(card.getName()), true);
+ return cards.getOrDefault(cardName, 0);
+ }
+
+ public boolean canBeBuilt(final Deck deck)
+ {
+ final Map<String, Integer> deckCards = new HashMap<>();
+
+ for (final Entry<PaperCard, Integer> card : deck.getAllCardsInASinglePool())
+ {
+ Integer count = deckCards.getOrDefault(card.getKey().getName(), 0);
+ count += card.getValue();
+ deckCards.put(card.getKey().getName(), count);
+ }
+
+ for (final Entry<String, Integer> deckCard : deckCards.entrySet())
+ if (deckCard.getValue() > cards.getOrDefault(deckCard.getKey(), 0))
+ return false;
+
+ return true;
}
@Override
public void reload() throws IOException
{
- cardNames.clear();
+ cards.clear();
try (BufferedReader reader = new BufferedReader(new FileReader(cardPool)))
{
@@ -54,7 +81,16 @@ public class CardPoolService implements Reloadable
{
line = line.trim();
- final PaperCard card = FModel.getMagicDb().getCommonCards().getCard(line);
+ final Matcher matcher = PATTERN_LINE.matcher(line);
+ if (!matcher.matches())
+ throw new RuntimeException("Cannot parse line: " + line);
+
+ final String countStr = matcher.group("count");
+ final int count = countStr != null ? Integer.parseInt(countStr) : Integer.MAX_VALUE;
+
+ final String name = matcher.group("name");
+
+ final PaperCard card = FModel.getMagicDb().getCommonCards().getCard(name);
if (card == null)
{
log.warn("Unknown card {}, maybe a typo ?", line);
@@ -64,6 +100,7 @@ public class CardPoolService implements Reloadable
if (card.getRules().getAiHints().getRemAIDecks())
{
log.warn("{} is marked as non-playable by AI", card.getName());
+ continue;
}
if (!format.getFilterRules().apply(card))
{
@@ -71,7 +108,7 @@ public class CardPoolService implements Reloadable
continue;
}
- cardNames.add(card.getName());
+ cards.put(card.getName(), count);
}
}
}
diff --git a/src/main/java/fr/kevincorvisier/mtg/gdb/ai/GeneticAlgorithm.java b/src/main/java/fr/kevincorvisier/mtg/gdb/ai/GeneticAlgorithm.java
@@ -25,7 +25,6 @@ import lombok.extern.slf4j.Slf4j;
@RequiredArgsConstructor
public class GeneticAlgorithm implements Reloadable
{
-
protected final Random rnd = new Random();
// Operators
diff --git a/src/main/java/fr/kevincorvisier/mtg/gdb/population/NextGenerationService.java b/src/main/java/fr/kevincorvisier/mtg/gdb/population/NextGenerationService.java
@@ -8,11 +8,11 @@ import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import forge.deck.Deck;
-import fr.kevincorvisier.mtg.gdb.ai.CardPoolService;
import fr.kevincorvisier.mtg.gdb.operators.crossover.Crossover;
import fr.kevincorvisier.mtg.gdb.operators.mutation.Mutation;
import fr.kevincorvisier.mtg.gdb.operators.selection.HybridSelection;
import fr.kevincorvisier.mtg.gdb.operators.selection.Selection;
+import fr.kevincorvisier.mtg.gdb.utils.RandomDeckGenerator;
import fr.kevincorvisier.mtg.gdb.validation.IndividualValidationService;
import lombok.RequiredArgsConstructor;
@@ -34,7 +34,7 @@ public class NextGenerationService
private final Mutation mutation;
private final Selection selection = new HybridSelection();
- private final CardPoolService cardPool;
+ private final RandomDeckGenerator deckGenerator;
private final IndividualValidationService validationService;
public Population toNextGeneration(final Population population)
@@ -64,7 +64,7 @@ public class NextGenerationService
while (next.getSize() < targetSize)
{
- final Deck deck = cardPool.getRandomDeck();
+ final Deck deck = deckGenerator.getRandomDeck();
final Individual child = new Individual("g0_new_" + next.getSize(), deck);
if (validationService.validate(child, next))
diff --git a/src/main/java/fr/kevincorvisier/mtg/gdb/population/PopulationFactory.java b/src/main/java/fr/kevincorvisier/mtg/gdb/population/PopulationFactory.java
@@ -6,8 +6,8 @@ import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import forge.deck.Deck;
-import fr.kevincorvisier.mtg.gdb.ai.CardPoolService;
import fr.kevincorvisier.mtg.gdb.utils.MagicOnlineDeckLoader;
+import fr.kevincorvisier.mtg.gdb.utils.RandomDeckGenerator;
import fr.kevincorvisier.mtg.gdb.validation.IndividualValidationService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@@ -18,7 +18,7 @@ import lombok.extern.slf4j.Slf4j;
public class PopulationFactory
{
private final MagicOnlineDeckLoader forgeUtils;
- private final CardPoolService cardPool;
+ private final RandomDeckGenerator deckGenerator;
private final IndividualValidationService validationService;
@Value("${population.initialization.disk.directories}")
@@ -78,7 +78,7 @@ public class PopulationFactory
while (population.getSize() < maximumSize)
{
- final Individual individual = new Individual("g0_" + population.getSize(), cardPool.getRandomDeck());
+ final Individual individual = new Individual("g0_" + population.getSize(), deckGenerator.getRandomDeck());
if (validationService.validate(individual, population))
population.addIndividual(individual);
}
diff --git a/src/main/java/fr/kevincorvisier/mtg/gdb/utils/RandomDeckGenerator.java b/src/main/java/fr/kevincorvisier/mtg/gdb/utils/RandomDeckGenerator.java
@@ -0,0 +1,122 @@
+package fr.kevincorvisier.mtg.gdb.utils;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.stream.Collectors;
+
+import org.springframework.stereotype.Service;
+
+import forge.deck.Deck;
+import forge.deck.DeckgenUtil;
+import forge.item.PaperCard;
+import fr.kevincorvisier.mtg.gdb.ai.CardPoolService;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class RandomDeckGenerator
+{
+ private final CardPoolService cardPool;
+
+ public Deck getRandomDeck()
+ {
+ final Deck deck = DeckgenUtil.getRandomColorDeck(cardPool::contains, true);
+ if (cardPool.canBeBuilt(deck))
+ return deck;
+
+ final Map<String, Integer> deckCards = new HashMap<>();
+
+ for (final Entry<PaperCard, Integer> card : deck.getAllCardsInASinglePool())
+ {
+ Integer count = deckCards.getOrDefault(card.getKey().getName(), 0);
+ count += card.getValue();
+ deckCards.put(card.getKey().getName(), count);
+ }
+
+ capCardCountToPool(deckCards);
+ increaseCardCountToPool(deckCards);
+ increaseBasicLands(deckCards);
+
+ log.info("fixed: {}", deckCards);
+
+ final Deck fixedDeck = new Deck();
+ deckCards.entrySet().forEach(card -> fixedDeck.getMain().add(card.getKey(), card.getValue()));
+ return fixedDeck;
+ }
+
+ /**
+ * Reduce count for cards that exceeds their limit set in the pool
+ */
+ private void capCardCountToPool(final Map<String, Integer> deckCards)
+ {
+ for (final Entry<String, Integer> card : deckCards.entrySet())
+ {
+ final String cardName = card.getKey();
+ final int currentCount = card.getValue();
+
+ final int maxCount = cardPool.getMaxCount(cardName);
+
+ if (maxCount == 0)
+ deckCards.remove(cardName);
+ else if (currentCount > maxCount)
+ deckCards.put(cardName, maxCount);
+ }
+ }
+
+ private void increaseCardCountToPool(final Map<String, Integer> deckCards)
+ {
+ for (final String cardName : deckCards.keySet())
+ {
+ if (isBasicLand(cardName))
+ continue;
+
+ final int deckCount = count(deckCards);
+ if (deckCount >= 60)
+ return;
+
+ final int maxCount = cardPool.getMaxCount(cardName);
+ final int currentCount = deckCards.get(cardName);
+
+ if (maxCount > currentCount)
+ {
+ final int newCount = Math.min(maxCount, currentCount + (60 - deckCount));
+ deckCards.put(cardName, newCount);
+ log.info("{} set to {} (current: {}, max: {})", cardName, newCount, currentCount, maxCount);
+ }
+ }
+ }
+
+ private void increaseBasicLands(final Map<String, Integer> deckCards)
+ {
+ final Map<String, Integer> basics = deckCards.entrySet().stream().filter(card -> isBasicLand(card.getKey()))
+ .collect(Collectors.toUnmodifiableMap(card -> card.getKey(), card -> card.getValue()));
+
+ final int totalBasics = count(basics);
+ final int newTotalBasics = totalBasics + (60 - count(deckCards));
+
+ for (final Entry<String, Integer> basic : basics.entrySet())
+ {
+ final int currentCount = basic.getValue();
+ final float currentRatio = ((float) currentCount) / totalBasics;
+ final int newCount = Math.round(currentRatio * newTotalBasics);
+
+ deckCards.put(basic.getKey(), newCount);
+ log.info("{} set to {} (current: {})", basic.getKey(), newCount, currentCount);
+ }
+ }
+
+ private static int count(final Map<String, Integer> cards)
+ {
+ return cards.values().stream().reduce(0, Math::addExact);
+ }
+
+ private static boolean isBasicLand(final String card)
+ {
+ return card.equals("Swamp") || card.equals("Plains") || card.equals("Island") || card.equals("Forest") || card.equals("Mountain")
+ || card.equals("Snow-Covered Swamp") || card.equals("Snow-Covered Plains") || card.equals("Snow-Covered Island")
+ || card.equals("Snow-Covered Forest") || card.equals("Snow-Covered Mountain");
+ }
+}
diff --git a/src/main/java/fr/kevincorvisier/mtg/gdb/validation/IndividualValidationService.java b/src/main/java/fr/kevincorvisier/mtg/gdb/validation/IndividualValidationService.java
@@ -16,6 +16,7 @@ import forge.deck.DeckFormat;
import forge.deck.DeckSection;
import forge.game.GameFormat;
import forge.item.PaperCard;
+import fr.kevincorvisier.mtg.gdb.ai.CardPoolService;
import fr.kevincorvisier.mtg.gdb.population.Individual;
import fr.kevincorvisier.mtg.gdb.population.Population;
import lombok.Data;
@@ -39,6 +40,7 @@ public class IndividualValidationService
private final Collection<Long> alreadyGenerated = new HashSet<>();
private final GameFormat format;
+ private final CardPoolService cardPool;
/**
* Avoid being in an infinite loop if no new decks can be validated
@@ -115,6 +117,9 @@ public class IndividualValidationService
if (uniqueCardsCount > maxUniqueCards)
return uniqueCardsCount + " unique cards, more than " + maxUniqueCards;
+ if (!cardPool.canBeBuilt(deck))
+ return "cannot be built with the current card pool";
+
for (final Condition condition : conditions)
{
conformanceProblem = condition.validate(deck);